diff --git a/DESCRIPTION b/DESCRIPTION index 8b6f478d..0b481e11 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -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 = diff --git a/NAMESPACE b/NAMESPACE index fd8ebfd8..9cb5bcb4 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -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) diff --git a/NEWS.md b/NEWS.md index cf2bf846..fb89e0ee 100644 --- a/NEWS.md +++ b/NEWS.md @@ -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 @@ -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 @@ -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 `_stacks` to the board-namespaced `-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 `_stacks` to the + board-namespaced `-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 @@ -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 @@ -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( = 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( = 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' diff --git a/R/block-server.R b/R/block-server.R index 562528ee..d9f208ba 100644 --- a/R/block-server.R +++ b/R/block-server.R @@ -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 @@ -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, @@ -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() diff --git a/R/board-server.R b/R/board-server.R index 6212059d..275fa0d3 100644 --- a/R/board-server.R +++ b/R/board-server.R @@ -22,21 +22,21 @@ #' Deferred evaluation leaves a block that nothing currently needs holding its #' last run — not only its result, but the conditions it reports. 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 -#' `sustain` payload components. Both name blocks, and core joins them, together -#' with their upstream closure over [board_links()] (without which they cannot +#' 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 `sustain` claim is held until its owner releases it. +#' `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. #' -#' Claims are keyed by owner, the `sustain` component mapping each owner to a -#' delta over the blocks it holds, so several consumers may hold the same block -#' and none of them writes another's claim: +#' 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: #' #' ```r #' update( #' list( -#' sustain = set_names( +#' eager = set_names( #' list(list(set = board_block_ids(board$board))), #' session$ns("preview") #' ) @@ -46,23 +46,26 @@ #' #' 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 part of a claim 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. +#' 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 -#' claims under two labels. A claim outlives the module that made it: core -#' drops a claimed block once it leaves the board, but an owner that goes away -#' without releasing holds what it held for the rest of the session. +#' 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. #' -#' Requests are orthogonal to the `required` visibility channel, so neither -#' competes with the front-end's gating, and nothing about what is on screen -#' changes. Because they carry no state change, they are also the one part of a -#' payload a locked board still accepts. +#' 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. @@ -83,10 +86,10 @@ #' 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 the front-end's `required` channel, so it cannot -#' turn a lazily evaluating board into an eagerly evaluating one. +#' 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 `sustain` names, +#' 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 @@ -112,29 +115,34 @@ board_server <- function(id, x, ...) { #' @param options Board options (`NULL` defaults to the union of board, block #' and registry sourced options) #' @param callbacks Single (or list of) callback function(s) registering -#' additional observers. Each receives a `visibility` list with three channels, -#' `required`, `visible` and `frozen`, each an environment of per-block -#' `reactiveVal`s (core keeps one per board block as blocks are added and -#' removed). Declare a block needed with `visibility$required[[id]](TRUE)` (or -#' `FALSE` for built but dormant) and report whether it is currently painted -#' with `visibility$visible[[id]](TRUE)` (or `FALSE` once built but off screen, -#' leaving `NA` until it is first built); the board reads both to gate -#' construction, evaluation and rendering. Set -#' `visibility$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. A -#' callback also receives the `update` channel (see [board_update]), through -#' which it can request block evaluation or construction (see the Evaluation -#' requests and Construction requests sections). +#' 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 seeds `blocks` as that owner's eager set before the first +#' flush, and at most one callback may return one. Changes from then on travel +#' as an `eager` component under the same owner label through the `update` +#' channel each callback receives (see [board_update] and the Evaluation +#' requests section). Each callback also receives a `visibility` list of the +#' per-block channels `visible` and `frozen`, environments of `reactiveVal`s +#' (core keeps one per board block as blocks are added and removed). Report +#' whether a block is currently painted with `visibility$visible[[id]](TRUE)` +#' (or `FALSE` once built but off screen, leaving `NA` until 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. Set `visibility$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 (see [stack_ui()]) and is -#' the default, so a board that renders core's UI gates on its stacks and one -#' that does not is left alone -- it passes its own callbacks. A consumer that -#' wants both keeps it in the list rather than replacing it -- +#' 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 the gate declares the -#' initially open stacks before the first flush and reads that input only to -#' refine the declaration. +#' 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. #' @param callback_location Location of callback invocation (before or after #' plugins) #' @rdname board_server @@ -180,7 +188,7 @@ board_server.board <- function(id, x, plugins = board_plugins(x), rv$eval <- reactiveValues() vis <- list( - required = new.env(parent = emptyenv()), + gate = reactiveVal(NULL), visible = new.env(parent = emptyenv()), frozen = new.env(parent = emptyenv()) ) @@ -208,23 +216,20 @@ board_server.board <- function(id, x, plugins = board_plugins(x), # changed. rv$needed_slots <- new.env(parent = emptyenv()) - # The two request sets fed by the `evaluate` and `sustain` board update + # The two request sets fed by the `evaluate` and `eager` board update # components. Both join the needed set below; they differ in who lets go. # Core drops an `evaluating` entry once that block has had its evaluation - # pass (see the observer below), while `claims` holds one entry per claim - # owner until that owner releases it. + # pass (see the observer below), while `eager_blocks` holds each owner's + # set until that owner releases it. The front-end is one such owner. rv$evaluating <- reactiveVal(character()) - rv$claims <- reactiveVal(list()) + rv$eager_blocks <- reactiveVal(list()) observe( { - cur <- if (!gating_active(vis$required)) { + cur <- if (!gating_active(vis)) { TRUE } else { - upstream_blocks( - union(required_now(vis$required), requested_blocks(rv)), - rv$board - ) + upstream_blocks(requested_blocks(rv), rv$board) } old <- isolate(rv$needed()) @@ -286,23 +291,16 @@ board_server.board <- function(id, x, plugins = board_plugins(x), board_update <- reactiveVal() - cb_res <- set_names( - vector("list", length(callbacks)), - names(callbacks) - ) - cb_args <- c( rv_ro, - list(update = board_update, visibility = vis), + list(update = board_update, visibility = vis[c("visible", "frozen")]), dot_args, list(session = session) ) if (identical(callback_location, "start")) { - for (i in seq_along(callbacks)) { - cb_res[[i]] <- do.call(callbacks[[i]], cb_args) - } + cb_res <- run_callbacks(callbacks, cb_args, rv, vis) if (length(cb_res) == 1L) { cb_res <- cb_res[[1L]] @@ -461,9 +459,7 @@ board_server.board <- function(id, x, plugins = board_plugins(x), if (identical(callback_location, "end")) { - for (i in seq_along(callbacks)) { - cb_res[[i]] <- do.call(callbacks[[i]], cb_args) - } + cb_res <- run_callbacks(callbacks, cb_args, rv, vis) dot_args <- c(dot_args, cb_res) } @@ -671,7 +667,7 @@ construct_needed_blocks <- function(rv, mod_ed, mod_ct, args, vis) { observe( { - need <- needed_block_ids(rv, vis$required) + need <- needed_block_ids(rv) construct_blocks(need, rv, mod_ed, mod_ct, args, vis) } ) @@ -694,7 +690,7 @@ construct_blocks_in_background <- function(rv, mod_ed, mod_ct, args, vis) { started <<- TRUE - if (!isolate(gating_active(vis$required))) { + if (!isolate(gating_active(vis))) { construct_blocks(board_block_ids(rv$board), rv, mod_ed, mod_ct, args, vis) @@ -720,7 +716,7 @@ construct_blocks_in_background <- function(rv, mod_ed, mod_ct, args, vis) { } needed <- isolate( - intersect(remaining, needed_block_ids(rv, vis$required)) + intersect(remaining, needed_block_ids(rv)) ) if (length(needed)) { @@ -732,7 +728,7 @@ construct_blocks_in_background <- function(rv, mod_ed, mod_ct, args, vis) { return(invisible()) } - if (gating_active(vis$required) && !required_fulfilled(vis)) { + if (gating_active(vis) && !gate_fulfilled(vis, rv)) { return(invisible()) } @@ -772,7 +768,6 @@ background_construction_delay <- function() { add_vis_slots <- function(vis, ids) { for (id in ids) { - vis$required[[id]] <- reactiveVal(NA) vis$visible[[id]] <- reactiveVal(NA) vis$frozen[[id]] <- reactiveVal(FALSE) } @@ -782,10 +777,9 @@ add_vis_slots <- function(vis, ids) { rm_vis_slots <- function(vis, ids) { - gone <- intersect(ids, ls(vis$required)) + gone <- intersect(ids, ls(vis$visible)) if (length(gone)) { - rm(list = gone, envir = vis$required) rm(list = gone, envir = vis$visible) rm(list = gone, envir = vis$frozen) } @@ -793,30 +787,8 @@ rm_vis_slots <- function(vis, ids) { invisible() } -gating_active <- function(required) { - isTRUE(blockr_option("gate_visibility", TRUE)) && has_required(required) -} - -has_required <- function(required) { - length(ever_required(required)) > 0L -} - -ever_required <- function(required) { - ids <- ls(required) - ids[lgl_ply(ids, slot_declared, required)] -} - -slot_declared <- function(id, required) { - !is.na(required[[id]]()) -} - -required_now <- function(required) { - ids <- ls(required) - ids[lgl_ply(ids, slot_needed, required)] -} - -slot_needed <- function(id, required) { - isTRUE(required[[id]]()) +gating_active <- function(vis) { + isTRUE(blockr_option("gate_visibility", TRUE)) && not_null(vis$gate()) } is_visible <- function(x) { @@ -831,19 +803,20 @@ block_frozen <- function(id, vis) { isTRUE(vis$frozen[[id]]()) } -required_fulfilled <- function(vis) { - all(lgl_ply(required_now(vis$required), block_visible, vis)) +# Only the front-end's own eager blocks are compared against paint: those held +# by anyone else are blocks nobody is putting on screen, and holding the +# backlog for them would stall it for the rest of the session. +gate_fulfilled <- function(vis, rv) { + all(lgl_ply(rv$eager_blocks()[[vis$gate()]], block_visible, vis)) } validate_vis <- function(vis) { - for (id in ls(vis$required)) { - if (!valid_required(vis$required[[id]]())) { - blockr_abort( - "required[[{id}]] must be TRUE, FALSE or NA", - class = "invalid_required" - ) - } + if (!valid_gate(vis$gate())) { + blockr_abort( + "gate must be a string or NULL", + class = "invalid_gate" + ) } for (id in ls(vis$visible)) { @@ -867,8 +840,109 @@ validate_vis <- function(vis) { invisible() } -valid_required <- function(x) { - is.logical(x) && length(x) == 1L +valid_gate <- function(x) { + is.null(x) || (is_string(x) && !is.na(x) && nzchar(x)) +} + +#' @param owner Label under which the front-end holds its blocks eager, as it +#' would name itself in an `eager` component +#' @param blocks Block IDs the front-end needs evaluated from the start +#' @rdname board_server +#' @export +eager <- function(owner, blocks = character()) { + + if (is.null(owner) || !valid_gate(owner)) { + blockr_abort( + "Expecting the owner of eager blocks to be a nonempty string.", + class = "eager_owner_invalid" + ) + } + + if (!is.character(blocks)) { + blockr_abort( + "Expecting eager blocks to be named by a character vector.", + class = "eager_blocks_invalid" + ) + } + + structure(list(owner = owner, blocks = blocks), class = "eager_blocks") +} + +is_eager_blocks <- function(x) { + inherits(x, "eager_blocks") +} + +run_callbacks <- function(callbacks, args, rv, vis) { + + res <- lapply(callbacks, do.call, args) + + seed_eager_blocks(res, rv, vis) + + Filter(Negate(is.null), lapply(res, drop_eager_blocks)) +} + +# A callback returns its declaration on its own, or alongside the values it +# hands on to plugins; either way the declaration is core's to read, not a +# value to splice into their arguments. +callback_eager_blocks <- function(res) { + + if (is_eager_blocks(res)) { + return(list(res)) + } + + if (is.list(res) && !is.object(res)) { + return(Filter(is_eager_blocks, res)) + } + + list() +} + +drop_eager_blocks <- function(res) { + + if (is_eager_blocks(res)) { + return(NULL) + } + + if (is.list(res) && !is.object(res)) { + return(Filter(Negate(is_eager_blocks), res)) + } + + res +} + +seed_eager_blocks <- function(res, rv, vis) { + + declared <- do.call(c, lapply(res, callback_eager_blocks)) + + if (!length(declared)) { + return(invisible()) + } + + if (length(declared) > 1L) { + blockr_abort( + "Expecting at most one callback to return eager blocks, but ", + "{length(declared)} did: {chr_xtr(declared, 'owner')}.", + class = "eager_declaration_ambiguous" + ) + } + + decl <- declared[[1L]] + + validate_eager_delta( + list(set = decl$blocks), + decl$owner, + isolate(board_block_ids(rv$board)) + ) + + vis$gate(decl$owner) + + # Setup runs outside any reactive consumer, where reading a reactiveValues + # field errors in a live session (a mock one evaluates inside isolate()). + isolate( + rv$eager_blocks(filter_empty(set_names(list(decl$blocks), decl$owner))) + ) + + invisible() } valid_visible <- function(x) { @@ -880,7 +954,7 @@ valid_frozen <- function(x) { } requested_blocks <- function(rv) { - union(rv$evaluating(), unlst(rv$claims())) + union(rv$evaluating(), unlst(rv$eager_blocks())) } # A block owes an evaluation pass while anything it needs for a result -- itself @@ -901,10 +975,10 @@ id_request_components <- function() { } update_request_components <- function() { - c(id_request_components(), "sustain") + c(id_request_components(), "eager") } -needed_block_ids <- function(rv, required) { +needed_block_ids <- function(rv) { need <- rv$needed() @@ -912,7 +986,7 @@ needed_block_ids <- function(rv, required) { return(board_block_ids(rv$board)) } - union(need, ever_required(required)) + need } block_inputs_ready <- function(src_rv, blk, rv) { @@ -1067,8 +1141,8 @@ destroy_rm_blocks <- function(ids, rv, sess) { } rv$evaluating(setdiff(isolate(rv$evaluating()), ids)) - rv$claims( - filter_empty(lapply(isolate(rv$claims()), setdiff, ids)) + rv$eager_blocks( + filter_empty(lapply(isolate(rv$eager_blocks()), setdiff, ids)) ) invisible() @@ -1349,17 +1423,17 @@ add_blocks_to_stacks <- function(rv, add, session) { #' @section Request components: #' Three components carry a request rather than a state change: #' `evaluate`, a character vector of block IDs to evaluate once; -#' `sustain`, a list of per-owner deltas over the blocks that are to +#' `eager`, a list of per-owner deltas over the blocks that are to #' stay evaluated; and `construct`, a character vector of block IDs to -#' build without evaluating. Each `sustain` delta is `set`, `add` and +#' build without evaluating. Each `eager` delta is `set`, `add` and #' `rm` — `set` states that owner's whole set and is exclusive with the -#' other two — so no owner writes another's claim. The two evaluation +#' other two — so no owner overwrites another's set. The two evaluation #' components put the named blocks (and their upstream closure) into #' the eval set while `construct` leaves them `dormant`, and none of #' the three touches what the front-end shows — see the Evaluation #' requests and Construction requests sections of [board_server()]. #' All three resolve their IDs against the post-update block set, so a -#' payload may add a block and ask for it in one go. A `sustain` `rm` +#' payload may add a block and ask for it in one go. An `eager` `rm` #' is the exception, naming blocks to release rather than to evaluate, #' and so may name one the board no longer has. They are applied after #' the state delta, so a payload that edits a block and evaluates it @@ -1368,8 +1442,8 @@ add_blocks_to_stacks <- function(rv, add, session) { #' The three are independent sets rather than alternatives: a payload #' may name one block in several of them and core takes the union. #' Overlap is redundant rather than rejected, which it has to be — -#' claims are per-owner, so a consumer asking for a block cannot know -#' that another owner already holds it. +#' eager sets are per-owner, so a consumer asking for a block cannot +#' know that another owner already holds it. #' #' A locked board (see [is_board_locked()]) still accepts a payload of #' request components alone; one that also carries a state change is @@ -1558,8 +1632,8 @@ validate_board_update_structure <- function(payload, board) { validate_block_id_request(payload[[cmp]], ids, cmp) } - if ("sustain" %in% names(payload)) { - validate_board_update_sustain(payload[["sustain"]], ids) + if ("eager" %in% names(payload)) { + validate_board_update_eager(payload[["eager"]], ids) } } @@ -1599,34 +1673,34 @@ validate_block_id_request <- function(x, ids, cmp) { invisible() } -validate_board_update_sustain <- function(x, ids) { +validate_board_update_eager <- function(x, ids) { if (!is.list(x) || length(names(x)) != length(x) || !all(nzchar(names(x))) || anyDuplicated(names(x)) != 0L) { blockr_abort( - "Expecting a board update `sustain` component to be specified as a list ", - "of per-owner claim deltas with unique nonempty names.", - class = "board_update_sustain_owners_invalid" + "Expecting a board update `eager` component to be specified as a list ", + "of per-owner deltas with unique nonempty names.", + class = "board_update_eager_owners_invalid" ) } for (owner in names(x)) { - validate_claim_delta(x[[owner]], owner, ids) + validate_eager_delta(x[[owner]], owner, ids) } invisible() } -validate_claim_delta <- function(x, owner, ids) { +validate_eager_delta <- function(x, owner, ids) { exp_cmp <- c("set", "add", "rm") if (!is.list(x) || length(names(x)) != length(x) || !all(names(x) %in% exp_cmp)) { blockr_abort( - "Expecting the claim of owner {owner} to consist of components ", + "Expecting the `eager` delta of owner {owner} to consist of components ", "{exp_cmp}.", - class = "board_update_sustain_components_invalid" + class = "board_update_eager_components_invalid" ) } @@ -1634,18 +1708,18 @@ validate_claim_delta <- function(x, owner, ids) { if (!(is.null(x[[cmp]]) || is.character(x[[cmp]]))) { blockr_abort( - "Expecting the {cmp} component of the claim of owner {owner} to be ", - "specified as a character vector (or NULL).", - class = "board_update_sustain_component_invalid" + "Expecting the {cmp} component of the `eager` delta of owner {owner} ", + "to be specified as a character vector (or NULL).", + class = "board_update_eager_component_invalid" ) } } if ("set" %in% names(x) && any(c("add", "rm") %in% names(x))) { blockr_abort( - "Expecting the claim of owner {owner} to state a whole set via `set` or ", - "a delta via `add` and `rm`, but not both.", - class = "board_update_sustain_set_delta_clash" + "Expecting the `eager` delta of owner {owner} to state a whole set via ", + "`set` or a change via `add` and `rm`, but not both.", + class = "board_update_eager_set_delta_clash" ) } @@ -1653,21 +1727,21 @@ validate_claim_delta <- function(x, owner, ids) { if (length(both)) { blockr_abort( - "Expecting the claim of owner {owner} to either add or remove ", + "Expecting the `eager` delta of owner {owner} to either add or remove ", "{qty(both)}block{?s} {both}.", - class = "board_update_sustain_add_rm_clash" + class = "board_update_eager_add_rm_clash" ) } - # Only a claim has to name blocks that exist -- a release commonly follows - # the very removal that made it necessary. + # Only `set` and `add` have to name blocks that exist -- a release commonly + # follows the very removal that made it necessary. unknown <- setdiff(c(x$set, x$add), ids) if (length(unknown)) { blockr_abort( "Owner {owner} requested evaluation of unknown {qty(unknown)}", "block{?s} {unknown}.", - class = "board_update_sustain_unknown_id" + class = "board_update_eager_unknown_id" ) } @@ -2257,19 +2331,19 @@ apply_core_board_update <- function(rv, upd, session, apply_eval_requests <- function(rv, upd) { - deltas <- upd[["sustain"]] + deltas <- upd[["eager"]] if (length(deltas)) { - log_debug("updating block claims of owner{?s} {names(deltas)}") + log_debug("updating eager blocks of owner{?s} {names(deltas)}") - claims <- isolate(rv$claims()) + held <- isolate(rv$eager_blocks()) for (owner in names(deltas)) { - claims[[owner]] <- apply_claim_delta(claims[[owner]], deltas[[owner]]) + held[[owner]] <- apply_eager_delta(held[[owner]], deltas[[owner]]) } - rv$claims(filter_empty(claims)) + rv$eager_blocks(filter_empty(held)) } if (length(upd[["evaluate"]])) { @@ -2280,7 +2354,7 @@ apply_eval_requests <- function(rv, upd) { invisible() } -apply_claim_delta <- function(cur, delta) { +apply_eager_delta <- function(cur, delta) { if ("set" %in% names(delta)) { return(delta$set) diff --git a/R/stack-gate.R b/R/stack-gate.R index 51bda0fc..5ace7fdd 100644 --- a/R/stack-gate.R +++ b/R/stack-gate.R @@ -2,66 +2,59 @@ #' @export gate_stacks <- function() { - function(board, visibility, session = get_session(), ...) { + function(board, visibility, update, session = get_session(), ...) { - brd <- isolate(board$board) + observe(show_open_stacks(board, visibility, update, session)) - # A stackless board renders an accordion that never binds as an input, so - # nothing would ever arrive to refine a declaration made on its behalf and - # it would stay parked for the session. - if (has_length(board_stack_ids(brd))) { - seed_open_stacks(brd, visibility) - } - - observe(show_open_stacks(board, visibility, session)) - - NULL + open_stacks_eager(isolate(board$board), session) } } -# Declared before the first flush, because a board with no gate declared is one -# where every block is needed: a collapsed stack's blocks would otherwise -# evaluate once in the window before the accordion reports. Paint stays the -# client's to report -- claiming it here as well would let the construction -# backlog build against first paint. -seed_open_stacks <- function(board, vis) { +# A stackless board renders an accordion that never binds as an input, so +# nothing would ever arrive to refine an eager set declared on its behalf and +# it would stay parked for the session; it is left eager instead. +open_stacks_eager <- function(board, session) { - shown <- shown_block_ids(board, default_open_stacks(board_stacks(board))) - - for (id in ls(vis$required)) { - vis$required[[id]](id %in% shown) + if (!has_length(board_stack_ids(board))) { + return(NULL) } - invisible() + eager( + stack_gate_owner(session), + shown_block_ids(board, default_open_stacks(board_stacks(board))) + ) } -show_open_stacks <- function(board, vis, session) { +show_open_stacks <- function(board, vis, update, session) { open <- session$input[["stacks"]] # Read before this returns, so the observer wakes when the accordion first - # reports. Until it does, what stands is the declaration seeded above -- and - # for a board that renders its own UI and never binds the accordion, nothing - # at all. + # reports. Until it does, what stands is the set the callback declared -- + # and for a board that renders its own UI and never binds the accordion, + # nothing at all. if (!stacks_reported(session)) { return(invisible()) } brd <- board$board + owner <- stack_gate_owner(session) shown <- shown_block_ids(brd, open_stack_ids(open, brd, session)) - # A collapsed stack's blocks are parked rather than dropped: `FALSE` keeps - # them built and ready to show again, where an `NA` slot would leave them - # unbuilt. - for (id in ls(vis$required)) { - vis$required[[id]](id %in% shown) + update(list(eager = set_names(list(list(set = shown)), owner))) + + for (id in ls(vis$visible)) { vis$visible[[id]](id %in% shown) } invisible() } +stack_gate_owner <- function(session) { + session$ns("gate_stacks") +} + # The accordion input reads NULL both before it has bound and once the user has # collapsed every stack; only the registered input name tells the two apart. stacks_reported <- function(session) { diff --git a/inst/examples/board/code/app.R b/inst/examples/board/code/app.R index 51d1ffc0..028bf672 100644 --- a/inst/examples/board/code/app.R +++ b/inst/examples/board/code/app.R @@ -12,7 +12,6 @@ serve( "my_board", callbacks = function(board, visibility, ...) { - visibility$required[["a"]](TRUE) visibility$visible[["a"]](TRUE) shiny::exportTestValues( @@ -20,6 +19,6 @@ serve( status_b = reval_if(board$eval[["b"]]) ) - NULL + eager("front-end", "a") } ) diff --git a/man/block_server.Rd b/man/block_server.Rd index 89d579ac..af595dc4 100644 --- a/man/block_server.Rd +++ b/man/block_server.Rd @@ -69,11 +69,12 @@ standalone)} eval set (supplied by \code{\link[=board_server]{board_server()}}; defaults to always-needed when a block server is run standalone)} -\item{visibility}{Front-end channel bundle -- a list with three channels, -\code{required}, \code{visible} and \code{frozen}, each an environment of per-block -\code{reactiveVal}s, supplied by \code{\link[=board_server]{board_server()}} to gate rendering and to -freeze block inputs; \code{NULL} (the standalone default) leaves the block -ungated} +\item{visibility}{Front-end channel bundle -- a \code{gate} \code{reactiveVal} holding +the owner label of the front-end that made the board lazy, plus \code{visible} +and \code{frozen}, each an environment of per-block \code{reactiveVal}s, supplied by +\code{\link[=board_server]{board_server()}} to hold rendering until a block is painted and to freeze +block inputs; \code{NULL} (the standalone default) renders the block as soon as +it is ready} } \value{ Both \code{block_server()} and \code{expr_server()} return shiny server module @@ -143,58 +144,68 @@ from output, the behavior of which can be customized via the \code{\link[=block_output]{block_output()}} generic. The \code{\link[=block_ui]{block_ui()}} generic can then be used to control rendering of outputs. -A front-end (such as blockr.dock) drives per-block channels that -\code{\link[=board_server]{board_server()}} hands to the board callback as \code{visibility}. Two of them -gate what is built and shown: \code{required} (which blocks it needs built and -evaluated) and \code{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 \code{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 \emph{needed} set, the \code{required} blocks together with -their upstream closure over \code{\link[=board_links]{board_links()}} (recomputed only when -requirements or links change). A block's input data reactives stay -unfulfilled (they \code{\link[shiny:req]{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 \emph{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 \code{required} slot of \code{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 \code{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 +\code{\link[=eager]{eager()}} from the callback it registers with \code{\link[=board_server]{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 \code{gate_visibility} \code{\link[=blockr_option]{blockr_option()}} (default \code{TRUE}) to \code{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 \code{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 \code{\link[=board_server]{board_server()}}). + +Evaluation follows the \emph{needed} set, the blocks held eager together with +their upstream closure over \code{\link[=board_links]{board_links()}} (recomputed only when eager sets +or links change). A block's input data reactives stay unfulfilled (they +\code{\link[shiny:req]{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 \code{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 \emph{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 \code{board$blocks} handed to plugins +and callbacks, which simply see it appear once constructed. The background cadence is set by the \code{background_construction_delay} \code{\link[=blockr_option]{blockr_option()}} (milliseconds between successive blocks, default 50); a value of 0 disables -the staggering and builds every block up front. With nothing writing -\code{required} every block is needed and behavior is unchanged; the -\code{gate_visibility} \code{\link[=blockr_option]{blockr_option()}} (default \code{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 \code{\link[bslib:accordion]{bslib::accordion()}} which opens one stack and collapses the rest (see \code{\link[=stack_ui]{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. -\code{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 \code{\link[=board_server]{board_server()}}'s default \code{callbacks} value. Which stacks +The \code{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 \code{\link[=board_server]{board_server()}}'s default \code{callbacks} value. Which stacks render open is core's own decision (see \code{\link[=stack_ui]{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 \code{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 \code{gate_visibility} option to \code{FALSE} keeps this board +eager too. The same bundle carries a third channel, \code{frozen}, through which a front-end reports the blocks whose inputs it has hidden (for example a diff --git a/man/board_server.Rd b/man/board_server.Rd index 7f0ee807..6e30e333 100644 --- a/man/board_server.Rd +++ b/man/board_server.Rd @@ -3,6 +3,7 @@ \name{board_server} \alias{board_server} \alias{board_server.board} +\alias{eager} \alias{gate_stacks} \title{Board server} \usage{ @@ -18,6 +19,8 @@ board_server(id, x, ...) ... ) +eager(owner, blocks = character()) + gate_stacks() } \arguments{ @@ -33,32 +36,42 @@ gate_stacks() and registry sourced options)} \item{callbacks}{Single (or list of) callback function(s) registering -additional observers. Each receives a \code{visibility} list with three channels, -\code{required}, \code{visible} and \code{frozen}, each an environment of per-block -\code{reactiveVal}s (core keeps one per board block as blocks are added and -removed). Declare a block needed with \code{visibility$required[[id]](TRUE)} (or -\code{FALSE} for built but dormant) and report whether it is currently painted -with \code{visibility$visible[[id]](TRUE)} (or \code{FALSE} once built but off screen, -leaving \code{NA} until it is first built); the board reads both to gate -construction, evaluation and rendering. Set -\code{visibility$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. A -callback also receives the \code{update} channel (see \link{board_update}), through -which it can request block evaluation or construction (see the Evaluation -requests and Construction requests sections). +additional observers. A board is eager by default: it evaluates every block. +A callback makes it lazy by returning \code{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 seeds \code{blocks} as that owner's eager set before the first +flush, and at most one callback may return one. Changes from then on travel +as an \code{eager} component under the same owner label through the \code{update} +channel each callback receives (see \link{board_update} and the Evaluation +requests section). Each callback also receives a \code{visibility} list of the +per-block channels \code{visible} and \code{frozen}, environments of \code{reactiveVal}s +(core keeps one per board block as blocks are added and removed). Report +whether a block is currently painted with \code{visibility$visible[[id]](TRUE)} +(or \code{FALSE} once built but off screen, leaving \code{NA} until 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. Set \code{visibility$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: \code{gate_stacks()} reads the stack accordion (see \code{\link[=stack_ui]{stack_ui()}}) and is -the default, so a board that renders core's UI gates on its stacks and one -that does not is left alone -- it passes its own callbacks. A consumer that -wants both keeps it in the list rather than replacing it -- +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 -- \code{callbacks = list(gate_stacks(), my_callback)}, which is for a front-end -that does render the accordion: on a stacked board the gate declares the -initially open stacks before the first flush and reads that input only to -refine the declaration.} +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.} \item{callback_location}{Location of callback invocation (before or after plugins)} + +\item{owner}{Label under which the front-end holds its blocks eager, as it +would name itself in an \code{eager} component} + +\item{blocks}{Block IDs the front-end needs evaluated from the start} } \value{ A \code{board_server()} implementation (such as the default for the @@ -90,20 +103,20 @@ default \code{\link[=notify_user]{notify_user()}} plugin renders its toasts from Deferred evaluation leaves a block that nothing currently needs holding its last run — not only its result, but the conditions it reports. Anything that can reach the \code{\link[=board_update]{board_update()}} channel can ask for such a block to be brought -up to date, without putting it on screen, through the \code{evaluate} and -\code{sustain} payload components. Both name blocks, and core joins them, together -with their upstream closure over \code{\link[=board_links]{board_links()}} (without which they cannot +up to date, without putting it on screen, through the \code{evaluate} and \code{eager} +payload components. Both name blocks, and core joins them, together with +their upstream closure over \code{\link[=board_links]{board_links()}} (without which they cannot produce a result), to the eval set. They differ only in who lets go: an -\code{evaluate} request is a one-off that core drops once the block has run, while -a \code{sustain} claim is held until its owner releases it. +\code{evaluate} request is a one-off that core drops once the block has run, +while a block held \code{eager} stays evaluated until its owner releases it. -Claims are keyed by owner, the \code{sustain} component mapping each owner to a -delta over the blocks it holds, so several consumers may hold the same block -and none of them writes another's claim: +Eager blocks are keyed by owner, the \code{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: \if{html}{\out{
}}\preformatted{update( list( - sustain = set_names( + eager = set_names( list(list(set = board_block_ids(board$board))), session$ns("preview") ) @@ -113,23 +126,26 @@ and none of them writes another's claim: A delta is \code{set}, \code{add} and \code{rm}, of which \code{set} states that owner's entire set at once and cannot be combined with the other two. Releasing everything -is \code{set = character()}; releasing part of a claim is \code{rm}, which — unlike -\code{set} and \code{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. +is \code{set = character()}; releasing some blocks is \code{rm}, which — unlike \code{set} +and \code{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 \code{session$ns()} as above is what keeps owners unique without a registry, and lets one module hold two independent -claims under two labels. A claim outlives the module that made it: core -drops a claimed block once it leaves the board, but an owner that goes away -without releasing holds what it held for the rest of the session. - -Requests are orthogonal to the \code{required} visibility channel, so neither -competes with the front-end's gating, and nothing about what is on screen -changes. Because they carry no state change, they are also the one part of a -payload a locked board still accepts. +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 \code{\link[=eager]{eager()}} (see +\code{\link[=block_server]{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. @@ -151,10 +167,10 @@ dependency order and left \code{dormant}: 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 the front-end's \code{required} channel, so it cannot -turn a lazily evaluating board into an eagerly evaluating one. +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 \code{evaluate} or \code{sustain} names, +A block that the same payload adds, or that an \code{evaluate} or \code{eager} names, is already constructed — the add builds it directly, and evaluation demand joins the needed set, which the background constructor builds. Pairing \code{construct} with either is redundant rather than wrong. The component covers diff --git a/man/board_update.Rd b/man/board_update.Rd index 409f3ebd..4f72cacc 100644 --- a/man/board_update.Rd +++ b/man/board_update.Rd @@ -63,17 +63,17 @@ payload slots reach subclass augment / apply methods. Three components carry a request rather than a state change: \code{evaluate}, a character vector of block IDs to evaluate once; -\code{sustain}, a list of per-owner deltas over the blocks that are to +\code{eager}, a list of per-owner deltas over the blocks that are to stay evaluated; and \code{construct}, a character vector of block IDs to -build without evaluating. Each \code{sustain} delta is \code{set}, \code{add} and +build without evaluating. Each \code{eager} delta is \code{set}, \code{add} and \code{rm} — \code{set} states that owner's whole set and is exclusive with the -other two — so no owner writes another's claim. The two evaluation +other two — so no owner overwrites another's set. The two evaluation components put the named blocks (and their upstream closure) into the eval set while \code{construct} leaves them \code{dormant}, and none of the three touches what the front-end shows — see the Evaluation requests and Construction requests sections of \code{\link[=board_server]{board_server()}}. All three resolve their IDs against the post-update block set, so a -payload may add a block and ask for it in one go. A \code{sustain} \code{rm} +payload may add a block and ask for it in one go. An \code{eager} \code{rm} is the exception, naming blocks to release rather than to evaluate, and so may name one the board no longer has. They are applied after the state delta, so a payload that edits a block and evaluates it @@ -82,8 +82,8 @@ sees the edit. The three are independent sets rather than alternatives: a payload may name one block in several of them and core takes the union. Overlap is redundant rather than rejected, which it has to be — -claims are per-owner, so a consumer asking for a block cannot know -that another owner already holds it. +eager sets are per-owner, so a consumer asking for a block cannot +know that another owner already holds it. A locked board (see \code{\link[=is_board_locked]{is_board_locked()}}) still accepts a payload of request components alone; one that also carries a state change is diff --git a/tests/testthat/test-board-lock.R b/tests/testthat/test-board-lock.R index 5605f070..1f531c6e 100644 --- a/tests/testthat/test-board-lock.R +++ b/tests/testthat/test-board-lock.R @@ -76,10 +76,10 @@ test_that("a locked board still accepts block requests", { session$flushReact() # A request carries no state change, so the lock does not apply to it. - board_update(list(sustain = list(consumer = list(set = "a")))) + board_update(list(eager = list(consumer = list(set = "a")))) session$flushReact() - expect_identical(rv$claims(), list(consumer = "a")) + expect_identical(rv$eager_blocks(), list(consumer = "a")) expect_true(rv$last_update$ok) board_update(list(construct = "a")) @@ -91,14 +91,14 @@ test_that("a locked board still accepts block requests", { board_update( list( blocks = list(add = as_blocks(list(b = new_dataset_block("BOD")))), - sustain = list(consumer = list(set = character())) + eager = list(consumer = list(set = character())) ) ) session$flushReact() expect_false(rv$last_update$ok) expect_length(board_blocks(rv$board), 1L) - expect_identical(rv$claims(), list(consumer = "a")) + expect_identical(rv$eager_blocks(), list(consumer = "a")) }, args = list(x = board, plugins = list()) ) diff --git a/tests/testthat/test-board-server.R b/tests/testthat/test-board-server.R index aae59dec..d254db3d 100644 --- a/tests/testthat/test-board-server.R +++ b/tests/testthat/test-board-server.R @@ -804,86 +804,86 @@ test_that("update validation", { expect_error( validate_board_update( - list(sustain = list(list(set = "a"))), + list(eager = list(list(set = "a"))), new_board(blocks(a = new_dataset_block())) ), - class = "board_update_sustain_owners_invalid" + class = "board_update_eager_owners_invalid" ) expect_error( validate_board_update( - list(sustain = set_names(list(list(set = "a")), "")), + list(eager = set_names(list(list(set = "a")), "")), new_board(blocks(a = new_dataset_block())) ), - class = "board_update_sustain_owners_invalid" + class = "board_update_eager_owners_invalid" ) expect_error( validate_board_update( list( - sustain = set_names( + eager = set_names( list(list(set = "a"), list(add = "a")), c("board-code_export", "board-code_export") ) ), new_board(blocks(a = new_dataset_block())) ), - class = "board_update_sustain_owners_invalid" + class = "board_update_eager_owners_invalid" ) expect_error( validate_board_update( - list(sustain = list(owner = "a")), + list(eager = list(owner = "a")), new_board(blocks(a = new_dataset_block())) ), - class = "board_update_sustain_components_invalid" + class = "board_update_eager_components_invalid" ) expect_error( validate_board_update( - list(sustain = list(owner = list(claim = "a"))), + list(eager = list(owner = list(keep = "a"))), new_board(blocks(a = new_dataset_block())) ), - class = "board_update_sustain_components_invalid" + class = "board_update_eager_components_invalid" ) expect_error( validate_board_update( - list(sustain = list(owner = list(set = 1L))), + list(eager = list(owner = list(set = 1L))), new_board(blocks(a = new_dataset_block())) ), - class = "board_update_sustain_component_invalid" + class = "board_update_eager_component_invalid" ) expect_error( validate_board_update( - list(sustain = list(owner = list(set = "a", add = "a"))), + list(eager = list(owner = list(set = "a", add = "a"))), new_board(blocks(a = new_dataset_block())) ), - class = "board_update_sustain_set_delta_clash" + class = "board_update_eager_set_delta_clash" ) expect_error( validate_board_update( - list(sustain = list(owner = list(add = "a", rm = "a"))), + list(eager = list(owner = list(add = "a", rm = "a"))), new_board(blocks(a = new_dataset_block())) ), - class = "board_update_sustain_add_rm_clash" + class = "board_update_eager_add_rm_clash" ) expect_error( validate_board_update( - list(sustain = list(owner = list(add = "b"))), + list(eager = list(owner = list(add = "b"))), new_board(blocks(a = new_dataset_block())) ), - class = "board_update_sustain_unknown_id" + class = "board_update_eager_unknown_id" ) # Releasing is the one direction that may name a block the board no longer # has, so a removal that races a release cannot reject the payload. expect_silent( validate_board_update( - list(sustain = list(owner = list(rm = "b"))), + list(eager = list(owner = list(rm = "b"))), new_board(blocks(a = new_dataset_block())) ) ) @@ -937,12 +937,12 @@ test_that("a subclass payload slot is not taken for a core component", { # Unknown top-level keys reach subclass methods untouched, and `$` # partial-matches, so a slot whose name extends a core one would be # applied here without ever having been validated. - board_update(list(evaluate_all = "nope", sustain_all = "nope")) + board_update(list(evaluate_all = "nope", eager_all = "nope")) session$flushReact() expect_true(rv$last_update$ok) expect_length(rv$evaluating(), 0L) - expect_length(rv$claims(), 0L) + expect_length(rv$eager_blocks(), 0L) }, args = list(x = board, plugins = list()) ) diff --git a/tests/testthat/test-input-freeze.R b/tests/testthat/test-input-freeze.R index 329a6128..f1374bed 100644 --- a/tests/testthat/test-input-freeze.R +++ b/tests/testthat/test-input-freeze.R @@ -1,7 +1,7 @@ make_vis <- function(ids, frozen = character()) { vis <- list( - required = new.env(parent = emptyenv()), + gate = reactiveVal(NULL), visible = new.env(parent = emptyenv()), frozen = new.env(parent = emptyenv()) ) diff --git a/tests/testthat/test-plugin-code.R b/tests/testthat/test-plugin-code.R index 96a3e46b..c4b32007 100644 --- a/tests/testthat/test-plugin-code.R +++ b/tests/testthat/test-plugin-code.R @@ -158,9 +158,8 @@ test_that("show code builds the board without evaluating or gating it", { testServer( get_s3_method("board_server", board), { - vis$required[["a"]](TRUE) + board_update(list(construct = "b")) vis$visible[["a"]](TRUE) - vis$required[["b"]](FALSE) session$flushReact() expect_identical(reval_if(rv$eval[["b"]]), "dormant") @@ -175,20 +174,24 @@ test_that("show code builds the board without evaluating or gating it", { expect_identical(reval_if(rv$eval[["c"]]), "dormant") expect_identical(reval_if(rv$eval[["b"]]), "dormant") - # Neither the front-end's gating channel nor the claim set is touched - expect_false(vis$required[["b"]]()) - expect_true(is.na(vis$required[["c"]]())) - expect_length(rv$claims(), 0L) + # The front-end's eager set is left exactly as it was, and the export adds + # none of its own + expect_identical(rv$eager_blocks(), list(`front-end` = "a")) session$setInputs(`generate_code-code_eval` = 1) session$flushReact() - # The one-off runs them and hands them back, leaving nothing held + # The one-off runs them and hands them back, leaving nothing held beyond + # the front-end's own eager set expect_length(rv$evaluating(), 0L) - expect_length(rv$claims(), 0L) + expect_identical(rv$eager_blocks(), list(`front-end` = "a")) expect_identical(reval_if(rv$eval[["c"]]), "dormant") }, - args = list(x = board, plugins = board_plugins(board, "generate_code")) + args = list( + x = board, + plugins = board_plugins(board, "generate_code"), + callbacks = function(...) eager("front-end", "a") + ) ) }) @@ -213,9 +216,9 @@ test_that("show code requires the whole board, gating export on config", { get_s3_method("board_server", board), { for (id in c("a", "b")) { - vis$required[[id]](TRUE) vis$visible[[id]](TRUE) } + session$flushReact() read_only <- function() { @@ -239,7 +242,11 @@ test_that("show code requires the whole board, gating export on config", { ) ) }, - args = list(x = board, plugins = board_plugins(board, "generate_code")) + args = list( + x = board, + plugins = board_plugins(board, "generate_code"), + callbacks = function(...) eager("front-end", c("a", "b")) + ) ) out diff --git a/tests/testthat/test-visibility-gating.R b/tests/testthat/test-visibility-gating.R index 5ac49837..fc241267 100644 --- a/tests/testthat/test-visibility-gating.R +++ b/tests/testthat/test-visibility-gating.R @@ -191,11 +191,31 @@ constructed <- function(id) { id %in% probe_construct$ids } -require_blocks <- function(vis, ...) { +# The front-end under test: its callback makes the board lazy by returning its +# opening eager set, and it states demand from then on as an +# `eager` component under that label, exactly as any other consumer would. Each +# payload helper writes the channel once, since a second write before the next +# flush would clobber the first. +front_end <- "front-end" + +front_delta <- function(...) { + set_names(list(list(...)), front_end) +} - for (id in c(...)) { - vis$required[[id]](TRUE) - } +declare_eager <- function(...) { + eager(front_end, c(...)) +} + +require_blocks <- function(update, ...) { + + update(list(eager = front_delta(add = c(...)))) + + invisible() +} + +release_blocks <- function(update, ...) { + + update(list(eager = front_delta(rm = c(...)))) invisible() } @@ -209,16 +229,28 @@ render_blocks <- function(vis, ...) { invisible() } -park_blocks <- function(vis, ...) { +park_blocks <- function(update, vis, ...) { + + release_blocks(update, ...) for (id in c(...)) { - vis$required[[id]](FALSE) vis$visible[[id]](FALSE) } invisible() } +front_eager <- function(rv) { + rv$eager_blocks()[[front_end]] +} + +# The front-end holds an eager set like any other owner, so a test about what +# consumers hold reads past its entry rather than the whole set. +consumer_eager <- function(rv) { + held <- rv$eager_blocks() + held[setdiff(names(held), front_end)] +} + block_conditions <- function(rv, id, severity) { cnd <- rv$conditions() cnd[cnd$block == id & cnd$severity == severity, ] @@ -278,7 +310,7 @@ test_that("a producer gates evaluation and rendering on visibility", { { session$flushReact() - expect_setequal(required_now(vis$required), "b") + expect_setequal(front_eager(rv), "b") expect_true(evaluated("b")) expect_true(rendered("b")) @@ -292,7 +324,7 @@ test_that("a producer gates evaluation and rendering on visibility", { expect_false(evaluated("d")) expect_false(rendered("d")) - require_blocks(vis, "c", "d") + require_blocks(board_update, "c", "d") render_blocks(vis, "c", "d") session$flushReact() @@ -306,8 +338,8 @@ test_that("a producer gates evaluation and rendering on visibility", { x = board, plugins = list(), callbacks = function(visibility, ...) { - require_blocks(visibility, "b") render_blocks(visibility, "b") + declare_eager("b") } ) ) @@ -345,8 +377,8 @@ test_that("the gate_visibility option disables gating", { x = board, plugins = list(), callbacks = function(visibility, ...) { - require_blocks(visibility, "b") render_blocks(visibility, "b") + declare_eager("b") } ) ) @@ -396,8 +428,8 @@ test_that("a link change re-routes the pulled upstream", { x = board, plugins = list(), callbacks = function(visibility, ...) { - require_blocks(visibility, "b") render_blocks(visibility, "b") + declare_eager("b") } ) ) @@ -409,8 +441,6 @@ test_that("a needed round trip with unchanged inputs does not re-evaluate", { withr::local_options(blockr.background_construction_delay = 0) - vis_env <- NULL - board <- new_board( blocks = c( a = with_id(probe_source(), "a"), @@ -431,13 +461,13 @@ test_that("a needed round trip with unchanged inputs does not re-evaluate", { # Park the chain, as a view switch whose visibility updates land across # several flushes does: b goes un-needed, taking a with it ... - vis_env$required[["b"]](FALSE) + release_blocks(board_update, "b") session$flushReact() # ... and comes back. Nothing upstream changed, so nothing re-evaluates: # the unchanged-inputs guard returns the cached results instead of # re-running the block expressions. - vis_env$required[["b"]](TRUE) + require_blocks(board_update, "b") session$flushReact() expect_false(evaluated("a")) @@ -447,9 +477,8 @@ test_that("a needed round trip with unchanged inputs does not re-evaluate", { x = board, plugins = list(), callbacks = function(visibility, ...) { - vis_env <<- visibility - require_blocks(visibility, "b") render_blocks(visibility, "b") + declare_eager("b") } ) ) @@ -485,7 +514,7 @@ test_that("a dormant block reports stale when an upstream re-evaluates", { # Park r off-screen: it drops out of the eval set and goes dormant, while # a stays required (its panel is still open). - vis$required[["r"]](FALSE) + release_blocks(board_update, "r") session$flushReact() expect_identical(rv$eval[["r"]](), "dormant") @@ -514,8 +543,8 @@ test_that("a dormant block reports stale when an upstream re-evaluates", { x = board, plugins = list(), callbacks = function(visibility, ...) { - require_blocks(visibility, "a", "r") render_blocks(visibility, "a", "r") + declare_eager("a", "r") } ) ) @@ -549,8 +578,7 @@ test_that("a dormant block whose upstreams are unchanged stays dormant", { # Park the whole chain, as a view switch does: r and its upstream a both # go dormant. a's last result survives dormancy, so r's cached input still # matches and r is not stale. - vis$required[["a"]](FALSE) - vis$required[["r"]](FALSE) + release_blocks(board_update, "a", "r") session$flushReact() expect_identical(rv$eval[["a"]](), "dormant") @@ -560,8 +588,8 @@ test_that("a dormant block whose upstreams are unchanged stays dormant", { x = board, plugins = list(), callbacks = function(visibility, ...) { - require_blocks(visibility, "a", "r") render_blocks(visibility, "a", "r") + declare_eager("a", "r") } ) ) @@ -596,8 +624,7 @@ test_that("staleness propagates to the whole dormant downstream cone", { expect_identical(rv$eval[["r"]](), "ready") # Park b and r off-screen; a stays required so it re-evaluates below. - vis$required[["b"]](FALSE) - vis$required[["r"]](FALSE) + release_blocks(board_update, "b", "r") session$flushReact() expect_identical(rv$eval[["b"]](), "dormant") @@ -623,8 +650,8 @@ test_that("staleness propagates to the whole dormant downstream cone", { x = board, plugins = list(), callbacks = function(visibility, ...) { - require_blocks(visibility, "a", "b", "r") render_blocks(visibility, "a", "b", "r") + declare_eager("a", "b", "r") } ) ) @@ -653,7 +680,7 @@ test_that("re-routing a dormant block's input marks it stale", { expect_identical(rv$eval[["r"]](), "ready") # Park r; a and b stay required (both ready). - vis$required[["r"]](FALSE) + release_blocks(board_update, "r") session$flushReact() expect_identical(rv$eval[["r"]](), "dormant") @@ -676,8 +703,8 @@ test_that("re-routing a dormant block's input marks it stale", { x = board, plugins = list(), callbacks = function(visibility, ...) { - require_blocks(visibility, "a", "b", "r") render_blocks(visibility, "a", "b", "r") + declare_eager("a", "b", "r") } ) ) @@ -703,7 +730,7 @@ test_that("a stale block that re-evaluates is dormant when parked again", { { session$flushReact() - vis$required[["r"]](FALSE) + release_blocks(board_update, "r") session$flushReact() board_update( @@ -719,12 +746,12 @@ test_that("a stale block that re-evaluates is dormant when parked again", { # again, so parking it a second time leaves it dormant. Neither b's result # nor its status changed in between, so the verdict has to be recomputed # off r's own last evaluation. - require_blocks(vis, "r") + require_blocks(board_update, "r") session$flushReact() expect_identical(rv$eval[["r"]](), "ready") - vis$required[["r"]](FALSE) + release_blocks(board_update, "r") session$flushReact() expect_identical(rv$eval[["r"]](), "dormant") @@ -733,8 +760,8 @@ test_that("a stale block that re-evaluates is dormant when parked again", { x = board, plugins = list(), callbacks = function(visibility, ...) { - require_blocks(visibility, "a", "b", "r") render_blocks(visibility, "a", "b", "r") + declare_eager("a", "b", "r") } ) ) @@ -770,7 +797,7 @@ test_that("an evaluation request brings a stale block current", { # Park the whole a -> r chain off screen, then re-route a to a different # dataset: neither re-evaluates, both report the break as stale. - park_blocks(vis, "a", "r") + park_blocks(board_update, vis, "a", "r") session$flushReact() board_update( @@ -802,7 +829,7 @@ test_that("an evaluation request brings a stale block current", { # The request is spent, and nothing about what is on screen changed. expect_length(rv$evaluating(), 0L) - expect_false(vis$required[["r"]]()) + expect_false("r" %in% front_eager(rv)) expect_false(vis$visible[["r"]]()) expect_false(rendered("r")) }, @@ -811,8 +838,8 @@ test_that("an evaluation request brings a stale block current", { plugins = list(), callbacks = function(visibility, update, ...) { upd_channel <<- update - require_blocks(visibility, "s1", "s2", "a", "r") render_blocks(visibility, "s1", "s2", "a", "r") + declare_eager("s1", "s2", "a", "r") } ) ) @@ -848,7 +875,7 @@ test_that("an evaluation request evaluates a block edited while dormant", { expect_identical(rv$eval[["r"]](), "ready") expect_equal(nrow(block_conditions(rv, "r", "error")), 0L) - park_blocks(vis, "r") + park_blocks(board_update, vis, "r") session$flushReact() expect_identical(rv$eval[["r"]](), "dormant") @@ -880,8 +907,8 @@ test_that("an evaluation request evaluates a block edited while dormant", { x = board, plugins = list(), callbacks = function(visibility, ...) { - require_blocks(visibility, "s", "r") render_blocks(visibility, "s", "r") + declare_eager("s", "r") } ) ) @@ -900,7 +927,7 @@ test_that("an edit and a request in one payload evaluate the edit", { { session$flushReact() - park_blocks(vis, "r") + park_blocks(board_update, vis, "r") session$flushReact() reset_probes() @@ -924,8 +951,8 @@ test_that("an edit and a request in one payload evaluate the edit", { x = board, plugins = list(), callbacks = function(visibility, ...) { - require_blocks(visibility, "s", "r") render_blocks(visibility, "s", "r") + declare_eager("s", "r") } ) ) @@ -974,14 +1001,14 @@ test_that("an evaluation request builds the blocks it needs", { x = board, plugins = list(), callbacks = function(visibility, ...) { - require_blocks(visibility, "s") render_blocks(visibility, "s") + declare_eager("s") } ) ) }) -test_that("a required claim holds a block until it is released", { +test_that("an eager block stays evaluated until it is released", { reset_probes() @@ -1000,15 +1027,15 @@ test_that("a required claim holds a block until it is released", { { session$flushReact() - park_blocks(vis, "r") + park_blocks(board_update, vis, "r") session$flushReact() expect_identical(rv$eval[["r"]](), "dormant") reset_probes() - # A claim, unlike a one-off request, survives evaluation. - board_update(list(sustain = list(consumer = list(set = "r")))) + # An eager block, unlike a one-off request, survives evaluation. + board_update(list(eager = list(consumer = list(set = "r")))) session$flushReact() expect_identical(rv$eval[["r"]](), "ready") @@ -1016,29 +1043,29 @@ test_that("a required claim holds a block until it is released", { for (i in 1:3) session$flushReact() expect_identical(rv$eval[["r"]](), "ready") - expect_identical(rv$claims(), list(consumer = "r")) + expect_identical(consumer_eager(rv), list(consumer = "r")) # Releasing it hands the block back to the front-end's gating, which # parked it. - board_update(list(sustain = list(consumer = list(set = character())))) + board_update(list(eager = list(consumer = list(set = character())))) session$flushReact() expect_identical(rv$eval[["r"]](), "dormant") - expect_length(rv$claims(), 0L) + expect_length(consumer_eager(rv), 0L) expect_false(rendered("r")) }, args = list( x = board, plugins = list(), callbacks = function(visibility, ...) { - require_blocks(visibility, "s", "r") render_blocks(visibility, "s", "r") + declare_eager("s", "r") } ) ) }) -test_that("one owner's release leaves another owner's claim standing", { +test_that("one owner's release leaves another owner's eager set standing", { reset_probes() @@ -1057,48 +1084,133 @@ test_that("one owner's release leaves another owner's claim standing", { { session$flushReact() - park_blocks(vis, "r") + park_blocks(board_update, vis, "r") session$flushReact() expect_identical(rv$eval[["r"]](), "dormant") - board_update(list(sustain = list(one = list(set = "r")))) + board_update(list(eager = list(one = list(set = "r")))) session$flushReact() - board_update(list(sustain = list(two = list(add = "r")))) + board_update(list(eager = list(two = list(add = "r")))) session$flushReact() - expect_identical(rv$claims(), list(one = "r", two = "r")) + expect_identical(consumer_eager(rv), list(one = "r", two = "r")) expect_identical(rv$eval[["r"]](), "ready") # The block is held by two owners, so the first letting go does not - # release the second's claim. - board_update(list(sustain = list(one = list(set = character())))) + # release the second's hold. + board_update(list(eager = list(one = list(set = character())))) session$flushReact() - expect_identical(rv$claims(), list(two = "r")) + expect_identical(consumer_eager(rv), list(two = "r")) expect_identical(rv$eval[["r"]](), "ready") # Releasing the last block an owner holds drops the owner, whether it # says so with `rm` or by setting an empty set. - board_update(list(sustain = list(two = list(rm = "r")))) + board_update(list(eager = list(two = list(rm = "r")))) session$flushReact() - expect_length(rv$claims(), 0L) + expect_length(consumer_eager(rv), 0L) expect_identical(rv$eval[["r"]](), "dormant") }, args = list( x = board, plugins = list(), callbacks = function(visibility, ...) { - require_blocks(visibility, "s", "r") render_blocks(visibility, "s", "r") + declare_eager("s", "r") + } + ) + ) +}) + +test_that("a consumer's eager block does not make an eager board lazy", { + + reset_probes() + + withr::local_options(blockr.background_construction_delay = 0) + + board <- new_board( + blocks = c( + s = with_id(probe_source(), "s"), + a = with_id(probe_passthrough(), "a"), + b = with_id(probe_passthrough(), "b") + ), + links = links( + sa = new_link("s", "a", "data"), + sb = new_link("s", "b", "data") + ) + ) + + testServer( + get_s3_method("board_server", board), + { + session$flushReact() + + board_update(list(eager = list(consumer = list(set = "a")))) + session$flushReact() + + # No callback made the board lazy, so an eager block changes nothing: it + # says what one consumer wants evaluated, never that everything else may + # be parked. Turning the board lazy on it instead would leave b -- which + # nobody asked for -- dormant and blank on a board that has no front-end. + expect_true(rv$needed()) + expect_identical(rv$eval[["b"]](), "ready") + expect_true(rendered("b")) + }, + args = list(x = board, plugins = list()) + ) +}) + +test_that("a consumer cannot release what the front-end holds", { + + reset_probes() + + withr::local_options(blockr.background_construction_delay = 0) + + board <- new_board( + blocks = c( + s = with_id(probe_source(), "s"), + r = with_id(probe_passthrough(), "r") + ), + links = links(sr = new_link("s", "r", "data")) + ) + + testServer( + get_s3_method("board_server", board), + { + session$flushReact() + + expect_identical(front_eager(rv), "r") + + # A consumer holds the block the front-end is showing and then lets go. + # Sharing one channel, its write landed on the front-end's own state and + # its release took the front-end's demand with it; as one owner among + # several it can do neither. + board_update(list(eager = list(consumer = list(set = "r")))) + session$flushReact() + + board_update(list(eager = list(consumer = list(set = character())))) + session$flushReact() + + expect_identical(front_eager(rv), "r") + expect_length(consumer_eager(rv), 0L) + expect_setequal(rv$needed(), c("s", "r")) + expect_identical(rv$eval[["r"]](), "ready") + }, + args = list( + x = board, + plugins = list(), + callbacks = function(visibility, ...) { + render_blocks(visibility, "r") + declare_eager("r") } ) ) }) -test_that("removing a claimed block prunes it from every owner", { +test_that("removing an eager block prunes it from every owner", { reset_probes() @@ -1119,7 +1231,7 @@ test_that("removing a claimed block prunes it from every owner", { board_update( list( - sustain = list( + eager = list( one = list(set = c("s", "r")), two = list(set = "r") ) @@ -1130,25 +1242,25 @@ test_that("removing a claimed block prunes it from every owner", { board_update(list(blocks = list(rm = "r"))) session$flushReact() - # An owner left holding nothing is dropped, so a stale claim cannot + # An owner left holding nothing is dropped, so a stale set cannot # outlive the block it named. - expect_identical(rv$claims(), list(one = "s")) + expect_identical(consumer_eager(rv), list(one = "s")) expect_setequal(rv$needed(), "s") # The owner that lost its block still releases cleanly: `rm` names a # block the board no longer has, and that must not reject the payload. - board_update(list(sustain = list(two = list(rm = "r")))) + board_update(list(eager = list(two = list(rm = "r")))) session$flushReact() expect_true(rv$last_update$ok) - expect_identical(rv$claims(), list(one = "s")) + expect_identical(consumer_eager(rv), list(one = "s")) }, args = list( x = board, plugins = list(), callbacks = function(visibility, ...) { - require_blocks(visibility, "s") render_blocks(visibility, "s") + declare_eager("s") } ) ) @@ -1188,8 +1300,8 @@ test_that("a request for a block added in the same payload is honoured", { x = board, plugins = list(), callbacks = function(visibility, ...) { - require_blocks(visibility, "s") render_blocks(visibility, "s") + declare_eager("s") } ) ) @@ -1233,16 +1345,16 @@ test_that("a construction request builds a block without evaluating it", { expect_setequal(rv$needed(), "s") expect_identical(probe_construct$ids, c("s", "r")) - # Nor does the request touch the channel the front-end owns, which is what - # keeps it from flipping an ungated board into gated mode. - expect_true(is.na(vis$required[["r"]]())) + # Nor does the request join the eager set the front-end holds, which is + # what keeps it from parking what is on screen. + expect_identical(front_eager(rv), "s") }, args = list( x = board, plugins = list(), callbacks = function(visibility, ...) { - require_blocks(visibility, "s") render_blocks(visibility, "s") + declare_eager("s") } ) ) @@ -1271,7 +1383,7 @@ test_that("overlapping requests union rather than clash", { list( construct = "r", evaluate = "r", - sustain = list(one = list(set = "r")) + eager = list(one = list(set = "r")) ) ) session$flushReact() @@ -1279,23 +1391,23 @@ test_that("overlapping requests union rather than clash", { expect_true(rv$last_update$ok) expect_true(constructed("r")) expect_identical(rv$eval[["r"]](), "ready") - expect_identical(rv$claims(), list(one = "r")) + expect_identical(consumer_eager(rv), list(one = "r")) expect_length(rv$evaluating(), 0L) # A second consumer cannot know what the first holds, so a one-off over - # a block someone else claims must not be rejected either. + # a block someone else holds eager must not be rejected either. board_update(list(evaluate = "r")) session$flushReact() expect_true(rv$last_update$ok) - expect_identical(rv$claims(), list(one = "r")) + expect_identical(consumer_eager(rv), list(one = "r")) }, args = list( x = board, plugins = list(), callbacks = function(visibility, ...) { - require_blocks(visibility, "s") render_blocks(visibility, "s") + declare_eager("s") } ) ) @@ -1330,18 +1442,18 @@ test_that("a request naming an unknown block is rejected", { expect_false(rv$last_update$ok) expect_identical(rv$last_update$phase, "validate") - board_update(list(sustain = list(consumer = list(set = "nope")))) + board_update(list(eager = list(consumer = list(set = "nope")))) session$flushReact() expect_false(rv$last_update$ok) - expect_length(rv$claims(), 0L) + expect_length(consumer_eager(rv), 0L) - # A claim with no owner to release it is refused as well. - board_update(list(sustain = list(list(set = "a")))) + # An eager set with no owner to release it is refused as well. + board_update(list(eager = list(list(set = "a")))) session$flushReact() expect_false(rv$last_update$ok) - expect_length(rv$claims(), 0L) + expect_length(consumer_eager(rv), 0L) expect_setequal(rv$needed(), "a") }, @@ -1349,8 +1461,8 @@ test_that("a request naming an unknown block is rejected", { x = board, plugins = list(), callbacks = function(visibility, ...) { - require_blocks(visibility, "a") render_blocks(visibility, "a") + declare_eager("a") } ) ) @@ -1391,8 +1503,7 @@ test_that("a view switch does not re-evaluate shared upstream left needed", { # the shared upstream (src, mid) stays needed throughout. Only the newly # visible leaf evaluates -- the upstream slots never flip, so nothing # pulls the shared pipeline again. - vis$required[["t1"]](FALSE) - vis$required[["t2"]](TRUE) + board_update(list(eager = front_delta(add = "t2", rm = "t1"))) render_blocks(vis, "t2") session$flushReact() @@ -1404,8 +1515,8 @@ test_that("a view switch does not re-evaluate shared upstream left needed", { x = board, plugins = list(), callbacks = function(visibility, ...) { - require_blocks(visibility, "t1") render_blocks(visibility, "t1") + declare_eager("t1") } ) ) @@ -1441,10 +1552,10 @@ test_that("a variadic block skips re-evaluation on unchanged inputs", { # pull, but the element objects are the cached upstream results. Park the # block across separate flushes and bring it back: the by-reference skip # sees the same objects and nothing re-evaluates. - vis$required[["v"]](FALSE) + release_blocks(board_update, "v") session$flushReact() - vis$required[["v"]](TRUE) + require_blocks(board_update, "v") session$flushReact() expect_false(evaluated("a")) @@ -1455,8 +1566,8 @@ test_that("a variadic block skips re-evaluation on unchanged inputs", { x = board, plugins = list(), callbacks = function(visibility, ...) { - require_blocks(visibility, "v") render_blocks(visibility, "v") + declare_eager("v") } ) ) @@ -1491,8 +1602,8 @@ test_that("an off-screen data-observing block does not pull its upstream", { x = board, plugins = list(), callbacks = function(visibility, ...) { - require_blocks(visibility, "c") render_blocks(visibility, "c") + declare_eager("c") } ) ) @@ -1537,8 +1648,8 @@ test_that("an unrelated structural edit does not re-evaluate needed blocks", { x = board, plugins = list(), callbacks = function(visibility, ...) { - require_blocks(visibility, "b") render_blocks(visibility, "b") + declare_eager("b") } ) ) @@ -1581,8 +1692,8 @@ test_that("adding a block does not re-evaluate existing needed blocks", { x = board, plugins = list(), callbacks = function(visibility, ...) { - require_blocks(visibility, "b") render_blocks(visibility, "b") + declare_eager("b") } ) ) @@ -1616,8 +1727,8 @@ test_that("a variadic block receives its inputs as values, not reactives", { x = board, plugins = list(), callbacks = function(visibility, ...) { - require_blocks(visibility, "c") render_blocks(visibility, "c") + declare_eager("c") } ) ) @@ -1654,8 +1765,8 @@ test_that("an off-screen variadic block does not pull its inputs", { x = board, plugins = list(), callbacks = function(visibility, ...) { - require_blocks(visibility, "e") render_blocks(visibility, "e") + declare_eager("e") } ) ) @@ -1678,8 +1789,8 @@ ordered_board <- function() { } visible_b <- function(visibility, ...) { - require_blocks(visibility, "b") render_blocks(visibility, "b") + declare_eager("b") } test_that("the priority lane builds the needed set ahead of the backlog", { @@ -1705,8 +1816,8 @@ test_that("the priority lane builds the needed set ahead of the backlog", { x = ordered_board(), plugins = list(), callbacks = function(visibility, ...) { - require_blocks(visibility, "c") render_blocks(visibility, "c") + declare_eager("c") } ) ) @@ -1727,7 +1838,7 @@ test_that("opening a view pulls its blocks ahead of a gated backlog", { expect_false(constructed("c")) expect_false(constructed("d")) - require_blocks(vis, "c") + require_blocks(board_update, "c") render_blocks(vis, "c") session$flushReact() @@ -1737,7 +1848,9 @@ test_that("opening a view pulls its blocks ahead of a gated backlog", { args = list( x = ordered_board(), plugins = list(), - callbacks = function(visibility, ...) require_blocks(visibility, "b") + callbacks = function(...) { + declare_eager("b") + } ) ) }) @@ -1789,7 +1902,7 @@ test_that("an infinite background delay never fills in the background", { expect_false(constructed("c")) expect_false(constructed("d")) - require_blocks(vis, "c") + require_blocks(board_update, "c") render_blocks(vis, "c") session$flushReact() @@ -1834,13 +1947,14 @@ test_that("is_visible is an isTRUE check on the slot value", { expect_false(is_visible(NA)) }) -test_that("channel validators enforce the required and visible contracts", { +test_that("channel validators enforce the gate and visible contracts", { - expect_true(valid_required(TRUE)) - expect_true(valid_required(FALSE)) - expect_true(valid_required(NA)) - expect_false(valid_required("x")) - expect_false(valid_required(NA_character_)) + expect_true(valid_gate(NULL)) + expect_true(valid_gate("dock")) + expect_false(valid_gate(NA_character_)) + expect_false(valid_gate("")) + expect_false(valid_gate(c("a", "b"))) + expect_false(valid_gate(TRUE)) expect_true(valid_visible(TRUE)) expect_true(valid_visible(FALSE)) @@ -1854,74 +1968,188 @@ test_that("validate_vis hard-errors on an off-contract slot", { isolate({ vis <- list( - required = new.env(parent = emptyenv()), + gate = reactiveVal(NULL), visible = new.env(parent = emptyenv()) ) add_vis_slots(vis, "a") - vis$required[["a"]](1L) - expect_error(validate_vis(vis), class = "invalid_required") + vis$gate(1L) + expect_error(validate_vis(vis), class = "invalid_gate") - vis$required[["a"]](TRUE) + vis$gate("dock") vis$visible[["a"]]("main") expect_error(validate_vis(vis), class = "invalid_visible") }) }) -test_that("required_now returns the TRUE-required blocks", { +test_that("a board turns lazy on the declaration, not on an eager block", { isolate({ - req <- new.env(parent = emptyenv()) - req$a <- reactiveVal(TRUE) - req$b <- reactiveVal(FALSE) - req$c <- reactiveVal(NA) - req$d <- reactiveVal(TRUE) - - expect_setequal(required_now(req), c("a", "d")) - expect_length(required_now(new.env(parent = emptyenv())), 0L) - }) -}) + vis <- list(gate = reactiveVal(NULL)) -test_that("ever_required and has_required track declared (non-NA) slots", { + expect_false(gating_active(vis)) - isolate({ - req <- new.env(parent = emptyenv()) - req$a <- reactiveVal(TRUE) - req$b <- reactiveVal(FALSE) - req$c <- reactiveVal(NA) - - expect_setequal(ever_required(req), c("a", "b")) - expect_true(has_required(req)) - expect_false(has_required(new.env(parent = emptyenv()))) + vis$gate("dock") + expect_true(gating_active(vis)) + + withr::local_options(blockr.gate_visibility = FALSE) + expect_false(gating_active(vis)) }) }) -test_that("required_fulfilled holds only when every required block is shown", { +test_that("gate_fulfilled tracks the front-end's eager set alone", { isolate({ vis <- list( - required = new.env(parent = emptyenv()), + gate = reactiveVal("dock"), visible = new.env(parent = emptyenv()) ) - add_vis_slots(vis, c("a", "b")) - vis$required[["a"]](TRUE) - vis$required[["b"]](TRUE) + add_vis_slots(vis, c("a", "b", "c")) + + rv <- reactiveValues(eager_blocks = reactiveVal(list(dock = c("a", "b")))) vis$visible[["a"]](TRUE) vis$visible[["b"]](TRUE) - expect_true(required_fulfilled(vis)) + expect_true(gate_fulfilled(vis, rv)) vis$visible[["b"]](FALSE) - expect_false(required_fulfilled(vis)) + expect_false(gate_fulfilled(vis, rv)) - empty <- list( - required = new.env(parent = emptyenv()), - visible = new.env(parent = emptyenv()) - ) - expect_true(required_fulfilled(empty)) + # Another owner's eager block off screen never lands on screen, so + # holding the backlog for it would stall it for good. + vis$visible[["b"]](TRUE) + rv$eager_blocks(list(dock = c("a", "b"), consumer = "c")) + expect_true(gate_fulfilled(vis, rv)) + + rv$eager_blocks(list()) + expect_true(gate_fulfilled(vis, rv)) }) }) +test_that("a declared eager set is in place before the first flush", { + + reset_probes() + + local_mocked_bindings(schedule_construction = drive_construction) + + eager_at_first_flush <- NULL + + testServer( + get_s3_method("board_server", ordered_board()), + { + session$flushReact() + + # Seeded as the callbacks run, so the first construction pass already + # has it: only the eager set and its upstream are built ahead of the + # backlog, and nothing outside it evaluates. + expect_identical(eager_at_first_flush, list(`front-end` = "b")) + expect_identical(probe_construct$ids[1:2], c("a", "b")) + + expect_true(evaluated("b")) + expect_false(evaluated("c")) + expect_false(evaluated("d")) + }, + args = list( + x = ordered_board(), + plugins = list(), + callbacks = list( + function(visibility, ...) { + render_blocks(visibility, "b") + declare_eager("b") + }, + function(board, ...) { + observe(eager_at_first_flush <<- board$eager_blocks(), priority = Inf) + NULL + } + ) + ) + ) +}) + +test_that("a declaration travels alongside a callback's plugin arguments", { + + testServer( + get_s3_method("board_server", ordered_board()), + { + session$flushReact() + + expect_identical(rv$eager_blocks(), list(`front-end` = "b")) + + expect_identical(session$returned$extra, 42) + expect_false(any(c("owner", "blocks") %in% names(session$returned))) + expect_false(any(lgl_ply(session$returned, is_eager_blocks))) + }, + args = list( + x = ordered_board(), + plugins = list(), + callbacks = function(...) list(extra = 42, declare_eager("b")), + callback_location = "start" + ) + ) +}) + +test_that("callbacks cannot write the gate, only declare it", { + + seen <- NULL + + testServer( + get_s3_method("board_server", ordered_board()), + { + session$flushReact() + + expect_setequal(seen, c("visible", "frozen")) + expect_false(gating_active(vis)) + }, + args = list( + x = ordered_board(), + plugins = list(), + callbacks = function(visibility, ...) { + seen <<- names(visibility) + NULL + } + ) + ) +}) + +test_that("at most one callback declares itself the gating front-end", { + + expect_error( + testServer( + get_s3_method("board_server", ordered_board()), + session$flushReact(), + args = list( + x = ordered_board(), + plugins = list(), + callbacks = list( + function(...) eager("one", "b"), + function(...) eager("two", "c") + ) + ) + ), + class = "eager_declaration_ambiguous" + ) +}) + +test_that("a declared eager set is validated as any eager delta is", { + + expect_error( + testServer( + get_s3_method("board_server", ordered_board()), + session$flushReact(), + args = list( + x = ordered_board(), + plugins = list(), + callbacks = function(...) eager("front-end", "nope") + ) + ), + class = "board_update_eager_unknown_id" + ) + + expect_error(eager(""), class = "eager_owner_invalid") + expect_error(eager(NA_character_), class = "eager_owner_invalid") + expect_error(eager("fe", 1L), class = "eager_blocks_invalid") +}) + test_that("the background waits for the front-end's rendered report", { reset_probes() @@ -1948,7 +2176,9 @@ test_that("the background waits for the front-end's rendered report", { args = list( x = ordered_board(), plugins = list(), - callbacks = function(visibility, ...) require_blocks(visibility, "b") + callbacks = function(...) { + declare_eager("b") + } ) ) }) @@ -2043,6 +2273,12 @@ stacked_board <- function() { # Mirrors what bslib's accordion input reports: the panel values of the open # stacks, and NULL rather than an empty vector once none are open. +# What core's own stack-gating callback holds eager, like any other owner, +# under the label gate_stacks() takes from the board session. +stack_eager <- function(rv, session) { + rv$eager_blocks()[[stack_gate_owner(session)]] +} + report_open_stacks <- function(session, ...) { ids <- c(...) @@ -2068,8 +2304,8 @@ test_that("core requires the open stacks and every unstacked block", { # Declared from what core renders open, before the client has reported # anything: with no gate in place every block is needed, and the # collapsed stack's would evaluate once in that window. - expect_true(gating_active(vis$required)) - expect_setequal(required_now(vis$required), c("a", "b", "e")) + expect_true(gating_active(vis)) + expect_setequal(stack_eager(rv, session), c("a", "b", "e")) expect_false(evaluated("c")) expect_false(rendered("c")) @@ -2077,7 +2313,7 @@ test_that("core requires the open stacks and every unstacked block", { report_open_stacks(session, "s1") session$flushReact() - expect_setequal(required_now(vis$required), c("a", "b", "e")) + expect_setequal(stack_eager(rv, session), c("a", "b", "e")) expect_setequal(rv$needed(), c("a", "b", "e")) expect_true(block_visible("b", vis)) @@ -2116,7 +2352,7 @@ test_that("expanding a stack requires its blocks and collapsing parks them", { session$flushReact() expect_setequal( - required_now(vis$required), + stack_eager(rv, session), c("a", "b", "c", "d", "e") ) @@ -2126,12 +2362,12 @@ test_that("expanding a stack requires its blocks and collapsing parks them", { report_open_stacks(session, "s2") session$flushReact() - expect_setequal(required_now(vis$required), c("c", "d", "e")) + expect_setequal(stack_eager(rv, session), c("c", "d", "e")) expect_setequal(rv$needed(), c("c", "d", "e")) # Parked rather than dropped: still built, so re-expanding shows them # without a rebuild. - expect_setequal(ever_required(vis$required), board_block_ids(rv$board)) + expect_setequal(names(rv$blocks), board_block_ids(rv$board)) expect_true(constructed("a")) expect_false(block_visible("a", vis)) @@ -2188,12 +2424,12 @@ test_that("a fully collapsed accordion is not read as one yet to report", { # Both states read as a NULL input: the accordion yet to report, and the # user having collapsed everything. What separates them is that the # first stands on what core rendered open. - expect_setequal(required_now(vis$required), c("a", "b", "e")) + expect_setequal(stack_eager(rv, session), c("a", "b", "e")) report_open_stacks(session) session$flushReact() - expect_setequal(required_now(vis$required), "e") + expect_setequal(stack_eager(rv, session), "e") expect_setequal(rv$needed(), "e") }, args = list(x = board, plugins = list()) @@ -2222,13 +2458,13 @@ test_that("a board without stacks requires every block", { # An accordion with no panels binds no input, so there would be nothing # to refine a declaration made here -- it is left ungated instead, which # costs nothing on a board where every block is unstacked anyway. - expect_false(gating_active(vis$required)) + expect_false(gating_active(vis)) expect_true(rv$needed()) report_open_stacks(session) session$flushReact() - expect_setequal(required_now(vis$required), c("a", "b")) + expect_setequal(stack_eager(rv, session), c("a", "b")) expect_true(evaluated("b")) expect_true(rendered("b")) @@ -2256,7 +2492,7 @@ test_that("the gate_visibility option disables stack gating", { report_open_stacks(session, "s1") session$flushReact() - expect_false(gating_active(vis$required)) + expect_false(gating_active(vis)) expect_true(rv$needed()) for (id in c("a", "b", "c", "d", "e")) { @@ -2341,7 +2577,7 @@ test_that("a front-end's own callbacks displace core's stack tracking", { # The board renders stacks and the option is on, yet core tracks nothing: # what gates is whatever the supplied callbacks do. - expect_false(gating_active(vis$required)) + expect_false(gating_active(vis)) expect_true(rv$needed()) for (id in c("a", "b", "c", "d", "e")) {