Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ Description: A framework for data manipulation and visualization using a
URL: https://bristolmyerssquibb.github.io/blockr.core/
BugReports: https://github.com/BristolMyersSquibb/blockr.core/issues
License: GPL (>= 3)
Remotes:
nbenn/typedjson
Encoding: UTF-8
Language: en-US
Roxygen: list(markdown = TRUE, packages = c("roxy.shinylive"), roclets =
Expand Down
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,7 @@ export(default_category)
export(default_icon)
export(default_stack_name)
export(dt_display)
export(eager)
export(edit_block)
export(edit_block_server)
export(edit_block_ui)
Expand Down
92 changes: 54 additions & 38 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# blockr.core 0.1.4

* 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).
* 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
Expand Down Expand Up @@ -54,12 +69,12 @@
* 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 with no gate declared
is one where every block is needed, 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 is left ungated, as before (#343).
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 dormant 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
Expand All @@ -72,31 +87,30 @@
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 marks the blocks of every open stack plus every unstacked block
required 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 and gates nothing
until that accordion reports, 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)`. The `gate_visibility` option
turns it off along with all other gating. 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).
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 `sustain` it retains
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 the front-end's `required` channel, 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 `required`
channel. A block parked with `required[[id]](FALSE)` was overwritten and
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
Expand Down Expand Up @@ -187,9 +201,10 @@
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 claims every block on the board, 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).
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
Expand Down Expand Up @@ -279,20 +294,21 @@
front-end can render it distinctly (e.g. a muted node badge); previously such
a block was indistinguishable from an up-to-date dormant one, so a break
introduced upstream stayed hidden until the block was visited (#310).
* Board updates gain two request components, `evaluate` and `sustain`, for
* Board updates gain two request components, `evaluate` and `eager`, for
evaluating a dormant 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 `sustain` claim is held until released. Claims 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 claim. 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 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'
Expand Down
109 changes: 60 additions & 49 deletions R/block-server.R
Original file line number Diff line number Diff line change
Expand Up @@ -58,58 +58,68 @@
#' [block_output()] generic. The [block_ui()] generic can then be used to
#' control rendering of outputs.
#'
#' A front-end (such as blockr.dock) drives per-block channels that
#' [board_server()] hands to the board callback as `visibility`. Two of them
#' gate what is built and shown: `required` (which blocks it needs built and
#' evaluated) and `visible` (which blocks it has arranged on screen).
#' Requirements are a cause the front-end -- and
#' core-side features such as code export -- declare; visibility is the effect
#' the front-end reports back once it has painted a block. Rendering is gated
#' on `visible`: the render observer is suspended while a block carries no
#' visible slot and resumed once the front-end writes a non-empty string for
#' it, starting suspended so nothing renders before the first report.
#' Evaluation is gated on the *needed* set, the `required` blocks together with
#' their upstream closure over [board_links()] (recomputed only when
#' requirements 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 required nor feeding a required block pulls no input and
#' stays fully quiescent: its result reactive, and any observer its expression
#' server registers on the incoming data, all short-circuit and do nothing. A
#' needed but off-screen block (one feeding a required block) evaluates but
#' does not render. Block-server *construction* is prioritized the same way:
#' the needed set is instantiated first so that first paint waits only for the
#' required blocks and their upstreams, and the remaining block servers are
#' built progressively in the background. That background pass holds until the
#' front-end reports every required block as visible, so it never competes with
#' first paint. A `required` slot of `FALSE` keeps a block built but dormant
#' (ever required, not needed now); an absent slot leaves it unbuilt. 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
#' 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, and any observer its expression server registers on the
#' incoming data, all short-circuit and do 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. With nothing writing
#' `required` every block is needed and behavior is unchanged; the
#' `gate_visibility` [blockr_option()] (default `TRUE`) turns gating off
#' entirely.
#' 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.
#' `gate_stacks()` reads that accordion back, marking the blocks of every open
#' stack plus every unstacked block required 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
#' 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 declares that set as the board server is set up, before the
#' first flush: a board with no gate declared is one where every block is
#' needed, and a collapsed stack's blocks would evaluate once in that window.
#' The accordion's report then refines the declaration rather than establishing
#' it. A board with no stacks binds no such input and has nothing to park, so
#' it is left ungated, as is a board driven by another front-end -- which
#' passes its own callbacks. Turning it off is the `gate_visibility` option
#' above, which already governs whether anything gates at all.
#' 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
Expand Down Expand Up @@ -150,11 +160,12 @@ block_server <- function(id, x, data = list(), ...) {
#' @param 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)
#' @param visibility Front-end channel bundle -- a list with three channels,
#' `required`, `visible` and `frozen`, each an environment of per-block
#' `reactiveVal`s, supplied by [board_server()] to gate rendering and to
#' freeze block inputs; `NULL` (the standalone default) leaves the block
#' ungated
#' @param visibility Front-end channel bundle -- a `gate` `reactiveVal` holding
#' the owner label of the front-end that made the board lazy, plus `visible`
#' and `frozen`, each an environment of per-block `reactiveVal`s, supplied by
#' [board_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
#' @rdname block_server
#' @export
block_server.block <- function(id, x, data = list(), block_id = id,
Expand Down Expand Up @@ -722,7 +733,7 @@ render_gate_observer <- function(id, visibility, render_obs, sess) {

observe(
{
do_render <- !gating_active(visibility$required) ||
do_render <- !gating_active(visibility) ||
block_visible(id, visibility)

if (do_render) render_obs$resume() else render_obs$suspend()
Expand Down
Loading
Loading