From b5adfbb29709ec1be0b3382d6e635752ea16ba55 Mon Sep 17 00:00:00 2001 From: Nicolas Bennett <3158446+nbenn@users.noreply.github.com> Date: Tue, 18 Aug 2026 20:18:51 +0000 Subject: [PATCH 1/6] Fold front-end demand into the one multi-owner claim set The per-block `required` channel expressed the same evaluation demand the `sustain` claims already carry, so core distinguished the front-end from every other consumer and the channel could quietly become multi-writer. The front-end now claims under an owner label like anyone else, asks for bare construction with `construct`, and declares that it drives visibility by writing that label into the new board-wide `visibility$gate` channel -- inferring gating from claims instead would let a consumer asking about one block park every other block on an ungated board. --- NEWS.md | 40 ++- R/block-server.R | 80 +++-- R/board-server.R | 135 ++++---- R/stack-gate.R | 51 ++- inst/examples/board/code/app.R | 5 +- man/block_server.Rd | 80 +++-- man/board_server.Rd | 43 ++- tests/testthat/test-input-freeze.R | 2 +- tests/testthat/test-plugin-code.R | 25 +- tests/testthat/test-visibility-gating.R | 410 +++++++++++++++--------- 10 files changed, 527 insertions(+), 344 deletions(-) diff --git a/NEWS.md b/NEWS.md index cf2bf846..8e8f5396 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: what it needs evaluated is + a `sustain` claim held under an owner label like any other consumer's, and + what it needs merely built is a `construct` request. Core no longer + distinguishes a front-end's demand from a code export's, and because no owner + writes another's claim, the channel cannot silently become multi-writer the + way `required` did. The three jobs the tri-state used to do in one slot are + now separate: gating activation is an explicit declaration a front-end makes + by writing its owner label into the new board-wide `visibility$gate` channel + -- inferring it from claims instead would let a consumer asking about one + block park every other block on an ungated board -- and construction demand is + the `construct` component. Reporting paint on `visible` is unchanged, and the + background pass still holds until every block the gating front-end claims is + reported painted. 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 @@ -72,14 +87,14 @@ 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, + callback claims the blocks of every open stack plus every unstacked block 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 through a `construct` request, 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)`. 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 @@ -92,11 +107,10 @@ long as it needed the expressions. Unlike `evaluate` and `sustain` 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 the gate declaration, 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 diff --git a/R/block-server.R b/R/block-server.R index 562528ee..2342a62c 100644 --- a/R/block-server.R +++ b/R/block-server.R @@ -58,39 +58,49 @@ #' [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 +#' A front-end (such as blockr.dock) declares that it will drive visibility by +#' writing an owner label into the `gate` channel of the `visibility` bundle +#' that [board_server()] hands to the board callback. That declaration, and +#' nothing else, is what flips the board from evaluating everything to +#' evaluating only what is needed; with no front-end every block is needed and +#' behaviour is unchanged, and the `gate_visibility` [blockr_option()] (default +#' `TRUE`) turns gating off entirely. It is written synchronously, while the +#' callback is set up, because it has to be in hand before the first flush +#' decides what to construct. +#' +#' Which blocks the front-end needs evaluated is not a channel of its own: it +#' is a `sustain` claim held 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()]). Blocks it wants built without being evaluated -- a card +#' it has created off screen, say -- are a `construct` request. +#' +#' Evaluation is gated on the *needed* set, the claimed blocks together with +#' their upstream closure over [board_links()] (recomputed only when claims 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 +#' claimed nor feeding a claimed 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 claimed block) evaluates but does not +#' render. +#' +#' Rendering is gated on `visible`, the per-block channel through which the +#' front-end reports what it has painted -- the effect, where a claim 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 claimed 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 +#' block it claims 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 @@ -150,11 +160,11 @@ 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` naming +#' the front-end driving visibility, plus `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 #' @rdname block_server #' @export block_server.block <- function(id, x, data = list(), block_id = id, @@ -722,7 +732,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..ab34964f 100644 --- a/R/board-server.R +++ b/R/board-server.R @@ -59,10 +59,13 @@ #' 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 `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. +#' A claim asks for evaluation and nothing else: nothing about what is on +#' screen changes. The front-end is an owner like any other -- it holds a claim +#' under the label it declared as `visibility$gate()` (see [block_server()]) -- +#' so core never distinguishes its demand from anyone else's, and a consumer +#' claiming a block cannot park what the front-end is showing. Because claims +#' 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,8 +86,8 @@ #' 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 the gate declaration, 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, #' is already constructed — the add builds it directly, and evaluation demand @@ -112,19 +115,20 @@ 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. Each receives a `visibility` list carrying a board-wide +#' `gate` `reactiveVal` alongside the per-block channels `visible` and `frozen`, +#' environments of `reactiveVal`s (core keeps one per board block as blocks are +#' added and removed). A front-end declares that it drives visibility by writing +#' its owner label, `visibility$gate("dock")`; until something does, every block +#' is needed. What it needs evaluated then travels as a `sustain` claim under +#' that label through the `update` channel it also receives (see [board_update] +#' and the Evaluation requests section), and what it needs merely built as a +#' `construct` request. 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 gates rendering on it, and +#' holds background construction until every claimed block is reported painted. +#' 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 @@ -180,7 +184,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()) ) @@ -212,19 +216,24 @@ board_server.board <- function(id, x, plugins = board_plugins(x), # 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. + # owner until that owner releases it. The front-end is one such owner. rv$evaluating <- reactiveVal(character()) rv$claims <- reactiveVal(list()) + # A gating front-end declares itself as it is set up but states its + # opening claim through a payload, which only applies at the end of that + # flush. Holding nothing and not having spoken yet are the same empty + # claim, so the difference is latched here: until it resolves, the + # background pass must not build a backlog that may be about to race the + # first paint. + rv$gate_claimed <- reactiveVal(FALSE) + 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()) @@ -671,7 +680,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 +703,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 +729,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 +741,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 +781,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 +790,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 +800,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 +816,21 @@ 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 gating front-end's own claim is compared against paint: a claim held +# by anyone else names blocks nobody is putting on screen, and holding the +# backlog for one would stall it for the rest of the session. +gate_fulfilled <- function(vis, rv) { + isTRUE(rv$gate_claimed()) && + all(lgl_ply(rv$claims()[[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 +854,8 @@ 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)) } valid_visible <- function(x) { @@ -904,7 +891,7 @@ update_request_components <- function() { c(id_request_components(), "sustain") } -needed_block_ids <- function(rv, required) { +needed_block_ids <- function(rv) { need <- rv$needed() @@ -912,7 +899,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) { @@ -2250,12 +2237,12 @@ apply_core_board_update <- function(rv, upd, session, construct_blocks(upd[["construct"]], rv, edit_block, ctrl_block, edit_plugin_args, vis) - apply_eval_requests(rv, upd) + apply_eval_requests(rv, upd, vis) invisible() } -apply_eval_requests <- function(rv, upd) { +apply_eval_requests <- function(rv, upd, vis) { deltas <- upd[["sustain"]] @@ -2270,6 +2257,10 @@ apply_eval_requests <- function(rv, upd) { } rv$claims(filter_empty(claims)) + + if (isTRUE(vis$gate() %in% names(deltas))) { + rv$gate_claimed(TRUE) + } } if (length(upd[["evaluate"]])) { diff --git a/R/stack-gate.R b/R/stack-gate.R index 51bda0fc..9c908278 100644 --- a/R/stack-gate.R +++ b/R/stack-gate.R @@ -2,7 +2,7 @@ #' @export gate_stacks <- function() { - function(board, visibility, session = get_session(), ...) { + function(board, visibility, update, session = get_session(), ...) { brd <- isolate(board$board) @@ -10,10 +10,10 @@ gate_stacks <- function() { # 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) + seed_open_stacks(brd, visibility, update, session) } - observe(show_open_stacks(board, visibility, session)) + observe(show_open_stacks(board, visibility, update, session)) NULL } @@ -21,21 +21,21 @@ gate_stacks <- function() { # 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 +# evaluate once in the window before the accordion reports. The gate itself is +# written here rather than sent, since a payload only applies at the end of the +# flush it is written in, by which time that window has passed. 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) { +seed_open_stacks <- function(board, vis, update, session) { shown <- shown_block_ids(board, default_open_stacks(board_stacks(board))) - for (id in ls(vis$required)) { - vis$required[[id]](id %in% shown) - } + vis$gate(stack_gate_owner(session)) - invisible() + claim_shown_blocks(board, shown, update, session) } -show_open_stacks <- function(board, vis, session) { +show_open_stacks <- function(board, vis, update, session) { open <- session$input[["stacks"]] @@ -51,17 +51,38 @@ show_open_stacks <- function(board, vis, 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) + vis$gate(stack_gate_owner(session)) + + claim_shown_blocks(brd, shown, update, session) + + for (id in ls(vis$visible)) { vis$visible[[id]](id %in% shown) } invisible() } +# A collapsed stack's blocks are parked rather than dropped -- held out of the +# claim but still built, so re-expanding shows them without a rebuild. Core +# ignores a `construct` request naming a block it has already built. +claim_shown_blocks <- function(board, shown, update, session) { + + owner <- stack_gate_owner(session) + + update( + list( + sustain = set_names(list(list(set = shown)), owner), + construct = setdiff(board_block_ids(board), 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..f812afd6 100644 --- a/inst/examples/board/code/app.R +++ b/inst/examples/board/code/app.R @@ -10,9 +10,10 @@ serve( ) ), "my_board", - callbacks = function(board, visibility, ...) { + callbacks = function(board, visibility, update, ...) { - visibility$required[["a"]](TRUE) + visibility$gate("front-end") + update(list(sustain = list(`front-end` = list(set = "a")))) visibility$visible[["a"]](TRUE) shiny::exportTestValues( diff --git a/man/block_server.Rd b/man/block_server.Rd index 89d579ac..84832d81 100644 --- a/man/block_server.Rd +++ b/man/block_server.Rd @@ -69,11 +69,11 @@ 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} naming +the front-end driving visibility, plus \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} } \value{ Both \code{block_server()} and \code{expr_server()} return shiny server module @@ -102,7 +102,7 @@ i.e. block user inputs and expression), and instantiation of the Each block carries an \emph{eval status} -- one of \code{dormant}, \code{stale}, \code{waiting}, \code{unset}, \code{failed} or \code{ready} -- which, together with its orthogonal front-end -visibility, determines its behavior. The status separates the two input +visibility, determines its behaviour. The status separates the two input kinds (data inputs from links, user inputs from \code{state}) and a genuine failure: \itemize{ @@ -143,39 +143,49 @@ 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 +A front-end (such as blockr.dock) declares that it will drive visibility by +writing an owner label into the \code{gate} channel of the \code{visibility} bundle +that \code{\link[=board_server]{board_server()}} hands to the board callback. That declaration, and +nothing else, is what flips the board from evaluating everything to +evaluating only what is needed; with no front-end every block is needed and +behaviour is unchanged, and the \code{gate_visibility} \code{\link[=blockr_option]{blockr_option()}} (default +\code{TRUE}) turns gating off entirely. It is written synchronously, while the +callback is set up, because it has to be in hand before the first flush +decides what to construct. + +Which blocks the front-end needs evaluated is not a channel of its own: it +is a \code{sustain} claim held 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()}}). Blocks it wants built without being evaluated -- a card +it has created off screen, say -- are a \code{construct} request. + +Evaluation is gated on the \emph{needed} set, the claimed blocks together with +their upstream closure over \code{\link[=board_links]{board_links()}} (recomputed only when claims 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 +claimed nor feeding a claimed 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 claimed block) evaluates but does not +render. + +Rendering is gated on \code{visible}, the per-block channel through which the +front-end reports what it has painted -- the effect, where a claim 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 claimed 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 +block it claims 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 diff --git a/man/board_server.Rd b/man/board_server.Rd index 7f0ee807..62058b14 100644 --- a/man/board_server.Rd +++ b/man/board_server.Rd @@ -33,6 +33,7 @@ gate_stacks() and registry sourced options)} \item{callbacks}{Single (or list of) callback function(s) registering +<<<<<<< HEAD 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 @@ -50,12 +51,27 @@ requests and Construction requests sections). 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 -- -\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 not is left alone -- it passes its own callbacks, and the +accordion input the callback waits on is never bound. A consumer that wants +both keeps it in the list rather than replacing it -- +\code{callbacks = list(gate_stacks(), my_callback)}.} +======= +additional observers. Each receives a \code{visibility} list carrying a board-wide +\code{gate} \code{reactiveVal} alongside 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). A front-end declares that it drives visibility by writing +its owner label, \code{visibility$gate("dock")}, as it is set up; until something +does, every block is needed. What it needs evaluated then travels as a +\code{sustain} claim under that label through the \code{update} channel it also +receives (see \link{board_update} and the Evaluation requests section), and what +it needs merely built as a \code{construct} request. 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 gates +rendering on it, and holds background construction until every claimed block +is reported painted. 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.} +>>>>>>> 780518d (Fold front-end demand into the one multi-owner claim set) \item{callback_location}{Location of callback invocation (before or after plugins)} @@ -126,10 +142,13 @@ 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. +A claim asks for evaluation and nothing else: nothing about what is on +screen changes. The front-end is an owner like any other -- it holds a claim +under the label it declared as \code{visibility$gate()} (see \code{\link[=block_server]{block_server()}}) -- +so core never distinguishes its demand from anyone else's, and a consumer +claiming a block cannot park what the front-end is showing. Because claims +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,8 +170,8 @@ 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 the gate declaration, 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, is already constructed — the add builds it directly, and evaluation demand 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..4641c69c 100644 --- a/tests/testthat/test-plugin-code.R +++ b/tests/testthat/test-plugin-code.R @@ -158,9 +158,11 @@ test_that("show code builds the board without evaluating or gating it", { testServer( get_s3_method("board_server", board), { - vis$required[["a"]](TRUE) + vis$gate("front-end") + board_update( + list(sustain = list(`front-end` = list(set = "a")), construct = "b") + ) vis$visible[["a"]](TRUE) - vis$required[["b"]](FALSE) session$flushReact() expect_identical(reval_if(rv$eval[["b"]]), "dormant") @@ -175,17 +177,17 @@ 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 claim is left exactly as it was, and the export adds + # none of its own + expect_identical(rv$claims(), 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 claim expect_length(rv$evaluating(), 0L) - expect_length(rv$claims(), 0L) + expect_identical(rv$claims(), list(`front-end` = "a")) expect_identical(reval_if(rv$eval[["c"]]), "dormant") }, args = list(x = board, plugins = board_plugins(board, "generate_code")) @@ -212,10 +214,15 @@ test_that("show code requires the whole board, gating export on config", { testServer( get_s3_method("board_server", board), { + vis$gate("front-end") + board_update( + list(sustain = list(`front-end` = list(set = c("a", "b")))) + ) + for (id in c("a", "b")) { - vis$required[[id]](TRUE) vis$visible[[id]](TRUE) } + session$flushReact() read_only <- function() { diff --git a/tests/testthat/test-visibility-gating.R b/tests/testthat/test-visibility-gating.R index 5ac49837..d28432a2 100644 --- a/tests/testthat/test-visibility-gating.R +++ b/tests/testthat/test-visibility-gating.R @@ -191,11 +191,32 @@ constructed <- function(id) { id %in% probe_construct$ids } -require_blocks <- function(vis, ...) { +# The front-end under test: it declares itself the gating owner and states its +# demand as a `sustain` claim under that label, exactly as any other consumer +# would. Each helper writes the payload channel once, since a second write +# before the next flush would clobber the first. +front_end <- "front-end" + +claim <- function(...) { + set_names(list(list(...)), front_end) +} - for (id in c(...)) { - vis$required[[id]](TRUE) - } +gate_blocks <- function(vis, update, ...) { + + vis$gate(front_end) + require_blocks(update, ...) +} + +require_blocks <- function(update, ...) { + + update(list(sustain = claim(add = c(...)))) + + invisible() +} + +release_blocks <- function(update, ...) { + + update(list(sustain = claim(rm = c(...)))) invisible() } @@ -209,16 +230,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() } +claimed <- function(rv) { + rv$claims()[[front_end]] +} + +# The front-end holds a claim like any other owner, so a test about what +# consumers hold reads past its entry rather than the whole set. +consumer_claims <- function(rv) { + claims <- rv$claims() + claims[setdiff(names(claims), front_end)] +} + block_conditions <- function(rv, id, severity) { cnd <- rv$conditions() cnd[cnd$block == id & cnd$severity == severity, ] @@ -278,7 +311,7 @@ test_that("a producer gates evaluation and rendering on visibility", { { session$flushReact() - expect_setequal(required_now(vis$required), "b") + expect_setequal(claimed(rv), "b") expect_true(evaluated("b")) expect_true(rendered("b")) @@ -292,7 +325,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() @@ -305,8 +338,8 @@ test_that("a producer gates evaluation and rendering on visibility", { args = list( x = board, plugins = list(), - callbacks = function(visibility, ...) { - require_blocks(visibility, "b") + callbacks = function(visibility, update, ...) { + gate_blocks(visibility, update, "b") render_blocks(visibility, "b") } ) @@ -344,8 +377,8 @@ test_that("the gate_visibility option disables gating", { args = list( x = board, plugins = list(), - callbacks = function(visibility, ...) { - require_blocks(visibility, "b") + callbacks = function(visibility, update, ...) { + gate_blocks(visibility, update, "b") render_blocks(visibility, "b") } ) @@ -395,8 +428,8 @@ test_that("a link change re-routes the pulled upstream", { args = list( x = board, plugins = list(), - callbacks = function(visibility, ...) { - require_blocks(visibility, "b") + callbacks = function(visibility, update, ...) { + gate_blocks(visibility, update, "b") render_blocks(visibility, "b") } ) @@ -409,8 +442,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 +462,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")) @@ -446,9 +477,8 @@ test_that("a needed round trip with unchanged inputs does not re-evaluate", { args = list( x = board, plugins = list(), - callbacks = function(visibility, ...) { - vis_env <<- visibility - require_blocks(visibility, "b") + callbacks = function(visibility, update, ...) { + gate_blocks(visibility, update, "b") render_blocks(visibility, "b") } ) @@ -485,7 +515,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") @@ -513,8 +543,8 @@ test_that("a dormant block reports stale when an upstream re-evaluates", { args = list( x = board, plugins = list(), - callbacks = function(visibility, ...) { - require_blocks(visibility, "a", "r") + callbacks = function(visibility, update, ...) { + gate_blocks(visibility, update, "a", "r") render_blocks(visibility, "a", "r") } ) @@ -549,8 +579,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") @@ -559,8 +588,8 @@ test_that("a dormant block whose upstreams are unchanged stays dormant", { args = list( x = board, plugins = list(), - callbacks = function(visibility, ...) { - require_blocks(visibility, "a", "r") + callbacks = function(visibility, update, ...) { + gate_blocks(visibility, update, "a", "r") render_blocks(visibility, "a", "r") } ) @@ -596,8 +625,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") @@ -622,8 +650,8 @@ test_that("staleness propagates to the whole dormant downstream cone", { args = list( x = board, plugins = list(), - callbacks = function(visibility, ...) { - require_blocks(visibility, "a", "b", "r") + callbacks = function(visibility, update, ...) { + gate_blocks(visibility, update, "a", "b", "r") render_blocks(visibility, "a", "b", "r") } ) @@ -653,7 +681,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") @@ -675,8 +703,8 @@ test_that("re-routing a dormant block's input marks it stale", { args = list( x = board, plugins = list(), - callbacks = function(visibility, ...) { - require_blocks(visibility, "a", "b", "r") + callbacks = function(visibility, update, ...) { + gate_blocks(visibility, update, "a", "b", "r") render_blocks(visibility, "a", "b", "r") } ) @@ -703,7 +731,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 +747,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") @@ -732,8 +760,8 @@ test_that("a stale block that re-evaluates is dormant when parked again", { args = list( x = board, plugins = list(), - callbacks = function(visibility, ...) { - require_blocks(visibility, "a", "b", "r") + callbacks = function(visibility, update, ...) { + gate_blocks(visibility, update, "a", "b", "r") render_blocks(visibility, "a", "b", "r") } ) @@ -770,7 +798,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 +830,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% claimed(rv)) expect_false(vis$visible[["r"]]()) expect_false(rendered("r")) }, @@ -811,7 +839,7 @@ 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") + gate_blocks(visibility, update, "s1", "s2", "a", "r") render_blocks(visibility, "s1", "s2", "a", "r") } ) @@ -848,7 +876,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") @@ -879,8 +907,8 @@ test_that("an evaluation request evaluates a block edited while dormant", { args = list( x = board, plugins = list(), - callbacks = function(visibility, ...) { - require_blocks(visibility, "s", "r") + callbacks = function(visibility, update, ...) { + gate_blocks(visibility, update, "s", "r") render_blocks(visibility, "s", "r") } ) @@ -900,7 +928,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() @@ -923,8 +951,8 @@ test_that("an edit and a request in one payload evaluate the edit", { args = list( x = board, plugins = list(), - callbacks = function(visibility, ...) { - require_blocks(visibility, "s", "r") + callbacks = function(visibility, update, ...) { + gate_blocks(visibility, update, "s", "r") render_blocks(visibility, "s", "r") } ) @@ -973,8 +1001,8 @@ test_that("an evaluation request builds the blocks it needs", { args = list( x = board, plugins = list(), - callbacks = function(visibility, ...) { - require_blocks(visibility, "s") + callbacks = function(visibility, update, ...) { + gate_blocks(visibility, update, "s") render_blocks(visibility, "s") } ) @@ -1000,7 +1028,7 @@ 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") @@ -1016,7 +1044,7 @@ 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_claims(rv), list(consumer = "r")) # Releasing it hands the block back to the front-end's gating, which # parked it. @@ -1024,14 +1052,14 @@ test_that("a required claim holds a block until it is released", { session$flushReact() expect_identical(rv$eval[["r"]](), "dormant") - expect_length(rv$claims(), 0L) + expect_length(consumer_claims(rv), 0L) expect_false(rendered("r")) }, args = list( x = board, plugins = list(), - callbacks = function(visibility, ...) { - require_blocks(visibility, "s", "r") + callbacks = function(visibility, update, ...) { + gate_blocks(visibility, update, "s", "r") render_blocks(visibility, "s", "r") } ) @@ -1057,7 +1085,7 @@ 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") @@ -1068,7 +1096,7 @@ test_that("one owner's release leaves another owner's claim standing", { board_update(list(sustain = list(two = list(add = "r")))) session$flushReact() - expect_identical(rv$claims(), list(one = "r", two = "r")) + expect_identical(consumer_claims(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 @@ -1076,7 +1104,7 @@ test_that("one owner's release leaves another owner's claim standing", { board_update(list(sustain = list(one = list(set = character())))) session$flushReact() - expect_identical(rv$claims(), list(two = "r")) + expect_identical(consumer_claims(rv), list(two = "r")) expect_identical(rv$eval[["r"]](), "ready") # Releasing the last block an owner holds drops the owner, whether it @@ -1084,20 +1112,105 @@ test_that("one owner's release leaves another owner's claim standing", { board_update(list(sustain = list(two = list(rm = "r")))) session$flushReact() - expect_length(rv$claims(), 0L) + expect_length(consumer_claims(rv), 0L) expect_identical(rv$eval[["r"]](), "dormant") }, args = list( x = board, plugins = list(), - callbacks = function(visibility, ...) { - require_blocks(visibility, "s", "r") + callbacks = function(visibility, update, ...) { + gate_blocks(visibility, update, "s", "r") render_blocks(visibility, "s", "r") } ) ) }) +test_that("a consumer claim does not gate an ungated board", { + + 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(sustain = list(consumer = list(set = "a")))) + session$flushReact() + + # Nothing declared a gate, so a claim is a no-op: it says what one + # consumer wants evaluated, never that everything else may be parked. + # Inferring the gate from claims 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(claimed(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(sustain = list(consumer = list(set = "r")))) + session$flushReact() + + board_update(list(sustain = list(consumer = list(set = character())))) + session$flushReact() + + expect_identical(claimed(rv), "r") + expect_length(consumer_claims(rv), 0L) + expect_setequal(rv$needed(), c("s", "r")) + expect_identical(rv$eval[["r"]](), "ready") + }, + args = list( + x = board, + plugins = list(), + callbacks = function(visibility, update, ...) { + gate_blocks(visibility, update, "r") + render_blocks(visibility, "r") + } + ) + ) +}) + test_that("removing a claimed block prunes it from every owner", { reset_probes() @@ -1132,7 +1245,7 @@ test_that("removing a claimed block prunes it from every owner", { # An owner left holding nothing is dropped, so a stale claim cannot # outlive the block it named. - expect_identical(rv$claims(), list(one = "s")) + expect_identical(consumer_claims(rv), list(one = "s")) expect_setequal(rv$needed(), "s") # The owner that lost its block still releases cleanly: `rm` names a @@ -1141,13 +1254,13 @@ test_that("removing a claimed block prunes it from every owner", { session$flushReact() expect_true(rv$last_update$ok) - expect_identical(rv$claims(), list(one = "s")) + expect_identical(consumer_claims(rv), list(one = "s")) }, args = list( x = board, plugins = list(), - callbacks = function(visibility, ...) { - require_blocks(visibility, "s") + callbacks = function(visibility, update, ...) { + gate_blocks(visibility, update, "s") render_blocks(visibility, "s") } ) @@ -1187,8 +1300,8 @@ test_that("a request for a block added in the same payload is honoured", { args = list( x = board, plugins = list(), - callbacks = function(visibility, ...) { - require_blocks(visibility, "s") + callbacks = function(visibility, update, ...) { + gate_blocks(visibility, update, "s") render_blocks(visibility, "s") } ) @@ -1233,15 +1346,15 @@ 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 claim the front-end holds, which is what + # keeps it from parking what is on screen. + expect_identical(claimed(rv), "s") }, args = list( x = board, plugins = list(), - callbacks = function(visibility, ...) { - require_blocks(visibility, "s") + callbacks = function(visibility, update, ...) { + gate_blocks(visibility, update, "s") render_blocks(visibility, "s") } ) @@ -1279,7 +1392,7 @@ 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_claims(rv), list(one = "r")) expect_length(rv$evaluating(), 0L) # A second consumer cannot know what the first holds, so a one-off over @@ -1288,13 +1401,13 @@ test_that("overlapping requests union rather than clash", { session$flushReact() expect_true(rv$last_update$ok) - expect_identical(rv$claims(), list(one = "r")) + expect_identical(consumer_claims(rv), list(one = "r")) }, args = list( x = board, plugins = list(), - callbacks = function(visibility, ...) { - require_blocks(visibility, "s") + callbacks = function(visibility, update, ...) { + gate_blocks(visibility, update, "s") render_blocks(visibility, "s") } ) @@ -1334,22 +1447,22 @@ test_that("a request naming an unknown block is rejected", { session$flushReact() expect_false(rv$last_update$ok) - expect_length(rv$claims(), 0L) + expect_length(consumer_claims(rv), 0L) # A claim with no owner to release it is refused as well. board_update(list(sustain = list(list(set = "a")))) session$flushReact() expect_false(rv$last_update$ok) - expect_length(rv$claims(), 0L) + expect_length(consumer_claims(rv), 0L) expect_setequal(rv$needed(), "a") }, args = list( x = board, plugins = list(), - callbacks = function(visibility, ...) { - require_blocks(visibility, "a") + callbacks = function(visibility, update, ...) { + gate_blocks(visibility, update, "a") render_blocks(visibility, "a") } ) @@ -1391,8 +1504,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(sustain = claim(add = "t2", rm = "t1"))) render_blocks(vis, "t2") session$flushReact() @@ -1403,8 +1515,8 @@ test_that("a view switch does not re-evaluate shared upstream left needed", { args = list( x = board, plugins = list(), - callbacks = function(visibility, ...) { - require_blocks(visibility, "t1") + callbacks = function(visibility, update, ...) { + gate_blocks(visibility, update, "t1") render_blocks(visibility, "t1") } ) @@ -1441,10 +1553,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")) @@ -1454,8 +1566,8 @@ test_that("a variadic block skips re-evaluation on unchanged inputs", { args = list( x = board, plugins = list(), - callbacks = function(visibility, ...) { - require_blocks(visibility, "v") + callbacks = function(visibility, update, ...) { + gate_blocks(visibility, update, "v") render_blocks(visibility, "v") } ) @@ -1490,8 +1602,8 @@ test_that("an off-screen data-observing block does not pull its upstream", { args = list( x = board, plugins = list(), - callbacks = function(visibility, ...) { - require_blocks(visibility, "c") + callbacks = function(visibility, update, ...) { + gate_blocks(visibility, update, "c") render_blocks(visibility, "c") } ) @@ -1536,8 +1648,8 @@ test_that("an unrelated structural edit does not re-evaluate needed blocks", { args = list( x = board, plugins = list(), - callbacks = function(visibility, ...) { - require_blocks(visibility, "b") + callbacks = function(visibility, update, ...) { + gate_blocks(visibility, update, "b") render_blocks(visibility, "b") } ) @@ -1580,8 +1692,8 @@ test_that("adding a block does not re-evaluate existing needed blocks", { args = list( x = board, plugins = list(), - callbacks = function(visibility, ...) { - require_blocks(visibility, "b") + callbacks = function(visibility, update, ...) { + gate_blocks(visibility, update, "b") render_blocks(visibility, "b") } ) @@ -1615,8 +1727,8 @@ test_that("a variadic block receives its inputs as values, not reactives", { args = list( x = board, plugins = list(), - callbacks = function(visibility, ...) { - require_blocks(visibility, "c") + callbacks = function(visibility, update, ...) { + gate_blocks(visibility, update, "c") render_blocks(visibility, "c") } ) @@ -1653,8 +1765,8 @@ test_that("an off-screen variadic block does not pull its inputs", { args = list( x = board, plugins = list(), - callbacks = function(visibility, ...) { - require_blocks(visibility, "e") + callbacks = function(visibility, update, ...) { + gate_blocks(visibility, update, "e") render_blocks(visibility, "e") } ) @@ -1677,8 +1789,8 @@ ordered_board <- function() { ) } -visible_b <- function(visibility, ...) { - require_blocks(visibility, "b") +visible_b <- function(visibility, update, ...) { + gate_blocks(visibility, update, "b") render_blocks(visibility, "b") } @@ -1704,8 +1816,8 @@ test_that("the priority lane builds the needed set ahead of the backlog", { args = list( x = ordered_board(), plugins = list(), - callbacks = function(visibility, ...) { - require_blocks(visibility, "c") + callbacks = function(visibility, update, ...) { + gate_blocks(visibility, update, "c") render_blocks(visibility, "c") } ) @@ -1727,7 +1839,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 +1849,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(visibility, update, ...) { + gate_blocks(visibility, update, "b") + } ) ) }) @@ -1789,7 +1903,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 +1948,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,71 +1969,64 @@ 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("gating activates on the declaration, not on a claim", { 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 gating owner's claim 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( + claims = reactiveVal(list(dock = c("a", "b"))), + gate_claimed = reactiveVal(TRUE) + ) 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 claim on an off-screen block never lands on screen, so + # holding the backlog for it would stall it for good. + vis$visible[["b"]](TRUE) + rv$claims(list(dock = c("a", "b"), consumer = "c")) + expect_true(gate_fulfilled(vis, rv)) + + rv$claims(list()) + expect_true(gate_fulfilled(vis, rv)) }) }) @@ -1948,7 +2056,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(visibility, update, ...) { + gate_blocks(visibility, update, "b") + } ) ) }) From 8eda07478fff53eab05c08a9c897efa6ae1ff769 Mon Sep 17 00:00:00 2001 From: Nicolas Bennett <3158446+nbenn@users.noreply.github.com> Date: Thu, 20 Aug 2026 13:15:07 +0000 Subject: [PATCH 2/6] Port core's stack gating onto the claim set The `gate_stacks()` callback landed on main while this branch was open and drove `visibility$required` directly. It now claims the blocks of every open stack under an owner label of its own, holds the collapsed ones built with a `construct` request, and declares the gate on the first accordion report -- the same moment the old slot writes used to activate gating. --- man/board_server.Rd | 45 ++++++++----------------- tests/testthat/test-visibility-gating.R | 28 +++++++++------ 2 files changed, 31 insertions(+), 42 deletions(-) diff --git a/man/board_server.Rd b/man/board_server.Rd index 62058b14..ab9184d1 100644 --- a/man/board_server.Rd +++ b/man/board_server.Rd @@ -33,20 +33,20 @@ gate_stacks() and registry sourced options)} \item{callbacks}{Single (or list of) callback function(s) registering -<<<<<<< HEAD -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. Each receives a \code{visibility} list carrying a board-wide +\code{gate} \code{reactiveVal} alongside 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). A front-end declares that it drives visibility by writing +its owner label, \code{visibility$gate("dock")}; until something does, every block +is needed. What it needs evaluated then travels as a \code{sustain} claim under +that label through the \code{update} channel it also receives (see \link{board_update} +and the Evaluation requests section), and what it needs merely built as a +\code{construct} request. 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 gates rendering on it, and +holds background construction until every claimed block is reported painted. +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 @@ -55,23 +55,6 @@ that does not is left alone -- it passes its own callbacks, and the accordion input the callback waits on is never bound. A consumer that wants both keeps it in the list rather than replacing it -- \code{callbacks = list(gate_stacks(), my_callback)}.} -======= -additional observers. Each receives a \code{visibility} list carrying a board-wide -\code{gate} \code{reactiveVal} alongside 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). A front-end declares that it drives visibility by writing -its owner label, \code{visibility$gate("dock")}, as it is set up; until something -does, every block is needed. What it needs evaluated then travels as a -\code{sustain} claim under that label through the \code{update} channel it also -receives (see \link{board_update} and the Evaluation requests section), and what -it needs merely built as a \code{construct} request. 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 gates -rendering on it, and holds background construction until every claimed block -is reported painted. 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.} ->>>>>>> 780518d (Fold front-end demand into the one multi-owner claim set) \item{callback_location}{Location of callback invocation (before or after plugins)} diff --git a/tests/testthat/test-visibility-gating.R b/tests/testthat/test-visibility-gating.R index d28432a2..72afd1ed 100644 --- a/tests/testthat/test-visibility-gating.R +++ b/tests/testthat/test-visibility-gating.R @@ -2153,6 +2153,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: a claim like any other owner's, +# under the label gate_stacks() takes from the board session. +stack_claim <- function(rv, session) { + rv$claims()[[stack_gate_owner(session)]] +} + report_open_stacks <- function(session, ...) { ids <- c(...) @@ -2178,8 +2184,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_claim(rv, session), c("a", "b", "e")) expect_false(evaluated("c")) expect_false(rendered("c")) @@ -2187,7 +2193,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_claim(rv, session), c("a", "b", "e")) expect_setequal(rv$needed(), c("a", "b", "e")) expect_true(block_visible("b", vis)) @@ -2226,7 +2232,7 @@ test_that("expanding a stack requires its blocks and collapsing parks them", { session$flushReact() expect_setequal( - required_now(vis$required), + stack_claim(rv, session), c("a", "b", "c", "d", "e") ) @@ -2236,12 +2242,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_claim(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)) @@ -2298,12 +2304,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_claim(rv, session), c("a", "b", "e")) report_open_stacks(session) session$flushReact() - expect_setequal(required_now(vis$required), "e") + expect_setequal(stack_claim(rv, session), "e") expect_setequal(rv$needed(), "e") }, args = list(x = board, plugins = list()) @@ -2338,7 +2344,7 @@ test_that("a board without stacks requires every block", { report_open_stacks(session) session$flushReact() - expect_setequal(required_now(vis$required), c("a", "b")) + expect_setequal(stack_claim(rv, session), c("a", "b")) expect_true(evaluated("b")) expect_true(rendered("b")) @@ -2366,7 +2372,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")) { @@ -2451,7 +2457,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")) { From b2da4f13f8d1e52ddbc804d4c8045cae8cf795b6 Mon Sep 17 00:00:00 2001 From: Nicolas Bennett <3158446+nbenn@users.noreply.github.com> Date: Thu, 27 Aug 2026 09:19:25 +0000 Subject: [PATCH 3/6] Seed the stack gate's claim before the first flush The #343 fix declares what core renders open as the board server is set up, which the claim set expresses as the gate declaration plus an opening claim. The declaration stays a synchronous write: a payload only applies at the end of the flush it is written in, by which time the window it closes has passed. --- man/board_server.Rd | 10 ++++++---- tests/testthat/test-visibility-gating.R | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/man/board_server.Rd b/man/board_server.Rd index ab9184d1..0852c52f 100644 --- a/man/board_server.Rd +++ b/man/board_server.Rd @@ -51,10 +51,12 @@ 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, and the -accordion input the callback waits on is never bound. A consumer that wants -both keeps it in the list rather than replacing it -- -\code{callbacks = list(gate_stacks(), my_callback)}.} +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 -- +\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.} \item{callback_location}{Location of callback invocation (before or after plugins)} diff --git a/tests/testthat/test-visibility-gating.R b/tests/testthat/test-visibility-gating.R index 72afd1ed..87c7a48f 100644 --- a/tests/testthat/test-visibility-gating.R +++ b/tests/testthat/test-visibility-gating.R @@ -2338,7 +2338,7 @@ 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) From ef1d124eba5461d360b81ccaf6e3f80482995a07 Mon Sep 17 00:00:00 2001 From: Nicolas Bennett <3158446+nbenn@users.noreply.github.com> Date: Mon, 28 Sep 2026 14:33:36 +0000 Subject: [PATCH 4/6] Declare the gating front-end at callback registration A payload applies at the end of the flush it is written in, after the first flush has decided what to construct, so a front-end whose only channel is `update` could not declare in time. The declaration now rides the one thing board_server() holds before any flush: a callback returns gate_claim(owner, blocks), and core seeds that opening claim as it runs the callbacks. That replaces the synchronous visibility$gate() write and the gate_claimed latch, which existed only because an opening claim sent by payload was indistinguishable from none until it landed. The stack gate declares its open stacks this way and no longer sends a `construct` for collapsed blocks, which built all of them in one flush where the background pass paces them. --- NAMESPACE | 1 + NEWS.md | 30 +-- R/block-server.R | 49 ++--- R/board-server.R | 175 ++++++++++----- R/stack-gate.R | 62 ++---- inst/examples/board/code/app.R | 6 +- man/block_server.Rd | 53 ++--- man/board_server.Rd | 48 +++-- tests/testthat/test-plugin-code.R | 22 +- tests/testthat/test-visibility-gating.R | 270 +++++++++++++++++------- 10 files changed, 450 insertions(+), 266 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index fd8ebfd8..9dc9f5e3 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -462,6 +462,7 @@ export(external_ctrl_vars) export(fatal_log_level) export(filebrowser_volumes) export(forward_ctor) +export(gate_claim) export(gate_stacks) export(generate_code) export(generate_code_server) diff --git a/NEWS.md b/NEWS.md index 8e8f5396..a17f46d4 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,19 +2,20 @@ * Evaluation demand is now one multi-owner set rather than two channels. The front-end's per-block `required` channel is gone: what it needs evaluated is - a `sustain` claim held under an owner label like any other consumer's, and - what it needs merely built is a `construct` request. Core no longer - distinguishes a front-end's demand from a code export's, and because no owner - writes another's claim, the channel cannot silently become multi-writer the - way `required` did. The three jobs the tri-state used to do in one slot are - now separate: gating activation is an explicit declaration a front-end makes - by writing its owner label into the new board-wide `visibility$gate` channel - -- inferring it from claims instead would let a consumer asking about one - block park every other block on an ungated board -- and construction demand is - the `construct` component. Reporting paint on `visible` is unchanged, and the - background pass still holds until every block the gating front-end claims is - reported painted. Breaking for any front-end that drives - `visibility$required` (#321). + a `sustain` claim held 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 writes another's claim, the channel cannot silently become + multi-writer the way `required` did. Whether a board gates is now declared + rather than inferred from whether anything had written demand: a front-end's + callback returns `gate_claim(owner, blocks)`, and core seeds `blocks` as that + owner's claim 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. A board whose callbacks declare nothing + is not gated, so a consumer claiming one block cannot park every other block + on it. 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 @@ -90,8 +91,7 @@ callback claims the blocks of every open stack plus every unstacked block 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 through a `construct` request, so re-expanding shows - them without a rebuild. It is `board_server()`'s default `callbacks` value, so + 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, diff --git a/R/block-server.R b/R/block-server.R index 2342a62c..e127db00 100644 --- a/R/block-server.R +++ b/R/block-server.R @@ -59,21 +59,22 @@ #' control rendering of outputs. #' #' A front-end (such as blockr.dock) declares that it will drive visibility by -#' writing an owner label into the `gate` channel of the `visibility` bundle -#' that [board_server()] hands to the board callback. That declaration, and -#' nothing else, is what flips the board from evaluating everything to -#' evaluating only what is needed; with no front-end every block is needed and -#' behaviour is unchanged, and the `gate_visibility` [blockr_option()] (default -#' `TRUE`) turns gating off entirely. It is written synchronously, while the -#' callback is set up, because it has to be in hand before the first flush -#' decides what to construct. +#' returning a [gate_claim()] from the callback it registers with +#' [board_server()], naming its owner label and the blocks it needs evaluated +#' from the start. That declaration, and nothing else, is what flips the board +#' from evaluating everything to evaluating only what is needed; a board whose +#' callbacks declare nothing has every block needed and behaves as it always +#' has, and the `gate_visibility` [blockr_option()] (default `TRUE`) turns +#' gating off entirely. Core reads the declaration as it runs the callbacks and +#' seeds the opening claim 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 is not a channel of its own: it -#' is a `sustain` claim held 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()]). Blocks it wants built without being evaluated -- a card -#' it has created off screen, say -- are a `construct` request. +#' Which blocks the front-end needs evaluated from then on is not a channel of +#' its own: it is a `sustain` claim held 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 is gated on the *needed* set, the claimed blocks together with #' their upstream closure over [board_links()] (recomputed only when claims or @@ -107,19 +108,19 @@ #' 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 +#' The `gate_stacks()` callback reads that accordion back, claiming the blocks +#' of every open stack plus every unstacked block 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 declares that set as its opening claim: a board with no gate +#' declared is one where every block is needed, and a collapsed stack's blocks +#' would otherwise evaluate once before the accordion reports. The accordion's +#' report then refines the claim 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 same bundle carries a third channel, `frozen`, through which a #' front-end reports the blocks whose inputs it has hidden (for example a diff --git a/R/board-server.R b/R/board-server.R index ab34964f..56055b5a 100644 --- a/R/board-server.R +++ b/R/board-server.R @@ -61,8 +61,8 @@ #' #' A claim asks for evaluation and nothing else: nothing about what is on #' screen changes. The front-end is an owner like any other -- it holds a claim -#' under the label it declared as `visibility$gate()` (see [block_server()]) -- -#' so core never distinguishes its demand from anyone else's, and a consumer +#' under the label it declared with [gate_claim()] (see [block_server()]) -- so +#' core never distinguishes its demand from any other owner's, and a consumer #' claiming a block cannot park what the front-end is showing. Because claims #' carry no state change, they are also the one part of a payload a locked #' board still accepts. @@ -115,20 +115,23 @@ 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 carrying a board-wide -#' `gate` `reactiveVal` alongside the per-block channels `visible` and `frozen`, -#' environments of `reactiveVal`s (core keeps one per board block as blocks are -#' added and removed). A front-end declares that it drives visibility by writing -#' its owner label, `visibility$gate("dock")`; until something does, every block -#' is needed. What it needs evaluated then travels as a `sustain` claim under -#' that label through the `update` channel it also receives (see [board_update] -#' and the Evaluation requests section), and what it needs merely built as a -#' `construct` request. 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 gates rendering on it, and -#' holds background construction until every claimed block is reported painted. -#' 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. +#' additional observers. A callback that drives visibility declares itself the +#' gating front-end by returning `gate_claim(owner, blocks)`, on its own or as +#' one element of a list whose other elements are passed on to plugins as +#' usual; core seeds `blocks` as that owner's claim before the first flush, and +#' at most one callback may declare. With no declaration every block is needed. +#' What the front-end needs evaluated from then on travels as a `sustain` claim +#' 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 gates +#' rendering on it, and holds background construction until every claimed block +#' is reported painted. 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 @@ -136,9 +139,8 @@ board_server <- function(id, x, ...) { #' 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 -- #' `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 declares the initially +#' open stacks as its opening claim and reads that input only to refine it. #' @param callback_location Location of callback invocation (before or after #' plugins) #' @rdname board_server @@ -220,14 +222,6 @@ board_server.board <- function(id, x, plugins = board_plugins(x), rv$evaluating <- reactiveVal(character()) rv$claims <- reactiveVal(list()) - # A gating front-end declares itself as it is set up but states its - # opening claim through a payload, which only applies at the end of that - # flush. Holding nothing and not having spoken yet are the same empty - # claim, so the difference is latched here: until it resolves, the - # background pass must not build a backlog that may be about to race the - # first paint. - rv$gate_claimed <- reactiveVal(FALSE) - observe( { cur <- if (!gating_active(vis)) { @@ -295,23 +289,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]] @@ -470,9 +457,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) } @@ -820,8 +805,7 @@ block_frozen <- function(id, vis) { # by anyone else names blocks nobody is putting on screen, and holding the # backlog for one would stall it for the rest of the session. gate_fulfilled <- function(vis, rv) { - isTRUE(rv$gate_claimed()) && - all(lgl_ply(rv$claims()[[vis$gate()]], block_visible, vis)) + all(lgl_ply(rv$claims()[[vis$gate()]], block_visible, vis)) } validate_vis <- function(vis) { @@ -858,6 +842,107 @@ valid_gate <- function(x) { is.null(x) || (is_string(x) && !is.na(x) && nzchar(x)) } +#' @param owner Label under which the gating front-end holds its claim, as it +#' would name itself in a `sustain` component +#' @param blocks Block IDs the front-end needs evaluated from the start +#' @rdname board_server +#' @export +gate_claim <- function(owner, blocks = character()) { + + if (is.null(owner) || !valid_gate(owner)) { + blockr_abort( + "Expecting a gate claim owner to be a nonempty string.", + class = "gate_claim_owner_invalid" + ) + } + + if (!is.character(blocks)) { + blockr_abort( + "Expecting a gate claim to name its blocks as a character vector.", + class = "gate_claim_blocks_invalid" + ) + } + + structure(list(owner = owner, blocks = blocks), class = "gate_claim") +} + +is_gate_claim <- function(x) { + inherits(x, "gate_claim") +} + +run_callbacks <- function(callbacks, args, rv, vis) { + + res <- lapply(callbacks, do.call, args) + + seed_gate_claim(res, rv, vis) + + Filter(Negate(is.null), lapply(res, drop_gate_claim)) +} + +# 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_gate_claims <- function(res) { + + if (is_gate_claim(res)) { + return(list(res)) + } + + if (is.list(res) && !is.object(res)) { + return(Filter(is_gate_claim, res)) + } + + list() +} + +drop_gate_claim <- function(res) { + + if (is_gate_claim(res)) { + return(NULL) + } + + if (is.list(res) && !is.object(res)) { + return(Filter(Negate(is_gate_claim), res)) + } + + res +} + +seed_gate_claim <- function(res, rv, vis) { + + claims <- do.call(c, lapply(res, callback_gate_claims)) + + if (!length(claims)) { + return(invisible()) + } + + if (length(claims) > 1L) { + blockr_abort( + "Expecting at most one callback to declare itself the gating ", + "front-end, but {length(claims)} did: {chr_xtr(claims, 'owner')}.", + class = "gate_claim_ambiguous" + ) + } + + claim <- claims[[1L]] + + validate_claim_delta( + list(set = claim$blocks), + claim$owner, + isolate(board_block_ids(rv$board)) + ) + + vis$gate(claim$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$claims(filter_empty(set_names(list(claim$blocks), claim$owner))) + ) + + invisible() +} + valid_visible <- function(x) { is.logical(x) && length(x) == 1L } @@ -2237,12 +2322,12 @@ apply_core_board_update <- function(rv, upd, session, construct_blocks(upd[["construct"]], rv, edit_block, ctrl_block, edit_plugin_args, vis) - apply_eval_requests(rv, upd, vis) + apply_eval_requests(rv, upd) invisible() } -apply_eval_requests <- function(rv, upd, vis) { +apply_eval_requests <- function(rv, upd) { deltas <- upd[["sustain"]] @@ -2257,10 +2342,6 @@ apply_eval_requests <- function(rv, upd, vis) { } rv$claims(filter_empty(claims)) - - if (isTRUE(vis$gate() %in% names(deltas))) { - rv$gate_claimed(TRUE) - } } if (length(upd[["evaluate"]])) { diff --git a/R/stack-gate.R b/R/stack-gate.R index 9c908278..c1f034ea 100644 --- a/R/stack-gate.R +++ b/R/stack-gate.R @@ -4,35 +4,25 @@ gate_stacks <- function() { function(board, visibility, update, session = get_session(), ...) { - brd <- isolate(board$board) - - # 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, update, session) - } - observe(show_open_stacks(board, visibility, update, session)) - NULL + open_stacks_claim(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. The gate itself is -# written here rather than sent, since a payload only applies at the end of the -# flush it is written in, by which time that window has passed. 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, update, session) { +# A stackless board renders an accordion that never binds as an input, so +# nothing would ever arrive to refine a claim made on its behalf and it would +# stay parked for the session; it is left ungated instead. +open_stacks_claim <- function(board, session) { - shown <- shown_block_ids(board, default_open_stacks(board_stacks(board))) - - vis$gate(stack_gate_owner(session)) + if (!has_length(board_stack_ids(board))) { + return(NULL) + } - claim_shown_blocks(board, shown, update, session) + gate_claim( + stack_gate_owner(session), + shown_block_ids(board, default_open_stacks(board_stacks(board))) + ) } show_open_stacks <- function(board, vis, update, session) { @@ -40,20 +30,19 @@ 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 claim 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)) - vis$gate(stack_gate_owner(session)) - - claim_shown_blocks(brd, shown, update, session) + update(list(sustain = set_names(list(list(set = shown)), owner))) for (id in ls(vis$visible)) { vis$visible[[id]](id %in% shown) @@ -62,23 +51,6 @@ show_open_stacks <- function(board, vis, update, session) { invisible() } -# A collapsed stack's blocks are parked rather than dropped -- held out of the -# claim but still built, so re-expanding shows them without a rebuild. Core -# ignores a `construct` request naming a block it has already built. -claim_shown_blocks <- function(board, shown, update, session) { - - owner <- stack_gate_owner(session) - - update( - list( - sustain = set_names(list(list(set = shown)), owner), - construct = setdiff(board_block_ids(board), shown) - ) - ) - - invisible() -} - stack_gate_owner <- function(session) { session$ns("gate_stacks") } diff --git a/inst/examples/board/code/app.R b/inst/examples/board/code/app.R index f812afd6..f54931f9 100644 --- a/inst/examples/board/code/app.R +++ b/inst/examples/board/code/app.R @@ -10,10 +10,8 @@ serve( ) ), "my_board", - callbacks = function(board, visibility, update, ...) { + callbacks = function(board, visibility, ...) { - visibility$gate("front-end") - update(list(sustain = list(`front-end` = list(set = "a")))) visibility$visible[["a"]](TRUE) shiny::exportTestValues( @@ -21,6 +19,6 @@ serve( status_b = reval_if(board$eval[["b"]]) ) - NULL + gate_claim("front-end", "a") } ) diff --git a/man/block_server.Rd b/man/block_server.Rd index 84832d81..2942cc04 100644 --- a/man/block_server.Rd +++ b/man/block_server.Rd @@ -102,7 +102,7 @@ i.e. block user inputs and expression), and instantiation of the Each block carries an \emph{eval status} -- one of \code{dormant}, \code{stale}, \code{waiting}, \code{unset}, \code{failed} or \code{ready} -- which, together with its orthogonal front-end -visibility, determines its behaviour. The status separates the two input +visibility, determines its behavior. The status separates the two input kinds (data inputs from links, user inputs from \code{state}) and a genuine failure: \itemize{ @@ -144,21 +144,22 @@ from output, the behavior of which can be customized via the control rendering of outputs. A front-end (such as blockr.dock) declares that it will drive visibility by -writing an owner label into the \code{gate} channel of the \code{visibility} bundle -that \code{\link[=board_server]{board_server()}} hands to the board callback. That declaration, and -nothing else, is what flips the board from evaluating everything to -evaluating only what is needed; with no front-end every block is needed and -behaviour is unchanged, and the \code{gate_visibility} \code{\link[=blockr_option]{blockr_option()}} (default -\code{TRUE}) turns gating off entirely. It is written synchronously, while the -callback is set up, because it has to be in hand before the first flush -decides what to construct. - -Which blocks the front-end needs evaluated is not a channel of its own: it -is a \code{sustain} claim held 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()}}). Blocks it wants built without being evaluated -- a card -it has created off screen, say -- are a \code{construct} request. +returning a \code{\link[=gate_claim]{gate_claim()}} 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. That declaration, and nothing else, is what flips the board +from evaluating everything to evaluating only what is needed; a board whose +callbacks declare nothing has every block needed and behaves as it always +has, and the \code{gate_visibility} \code{\link[=blockr_option]{blockr_option()}} (default \code{TRUE}) turns +gating off entirely. Core reads the declaration as it runs the callbacks and +seeds the opening claim 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 is a \code{sustain} claim held 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 is gated on the \emph{needed} set, the claimed blocks together with their upstream closure over \code{\link[=board_links]{board_links()}} (recomputed only when claims or @@ -192,19 +193,19 @@ 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 +The \code{gate_stacks()} callback reads that accordion back, claiming the blocks +of every open stack plus every unstacked block 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 declares that set as its opening claim: a board with no gate +declared is one where every block is needed, and a collapsed stack's blocks +would otherwise evaluate once before the accordion reports. The accordion's +report then refines the claim 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 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 0852c52f..1eba44b5 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{gate_claim} \alias{gate_stacks} \title{Board server} \usage{ @@ -18,6 +19,8 @@ board_server(id, x, ...) ... ) +gate_claim(owner, blocks = character()) + gate_stacks() } \arguments{ @@ -33,20 +36,23 @@ gate_stacks() and registry sourced options)} \item{callbacks}{Single (or list of) callback function(s) registering -additional observers. Each receives a \code{visibility} list carrying a board-wide -\code{gate} \code{reactiveVal} alongside 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). A front-end declares that it drives visibility by writing -its owner label, \code{visibility$gate("dock")}; until something does, every block -is needed. What it needs evaluated then travels as a \code{sustain} claim under -that label through the \code{update} channel it also receives (see \link{board_update} -and the Evaluation requests section), and what it needs merely built as a -\code{construct} request. 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 gates rendering on it, and -holds background construction until every claimed block is reported painted. -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. +additional observers. A callback that drives visibility declares itself the +gating front-end by returning \code{gate_claim(owner, blocks)}, on its own or as +one element of a list whose other elements are passed on to plugins as +usual; core seeds \code{blocks} as that owner's claim before the first flush, and +at most one callback may declare. With no declaration every block is needed. +What the front-end needs evaluated from then on travels as a \code{sustain} claim +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 gates +rendering on it, and holds background construction until every claimed block +is reported painted. 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 @@ -54,12 +60,16 @@ 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 -- \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 declares the initially +open stacks as its opening claim 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 gating front-end holds its claim, as it +would name itself in a \code{sustain} 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 @@ -129,8 +139,8 @@ without releasing holds what it held for the rest of the session. A claim asks for evaluation and nothing else: nothing about what is on screen changes. The front-end is an owner like any other -- it holds a claim -under the label it declared as \code{visibility$gate()} (see \code{\link[=block_server]{block_server()}}) -- -so core never distinguishes its demand from anyone else's, and a consumer +under the label it declared with \code{\link[=gate_claim]{gate_claim()}} (see \code{\link[=block_server]{block_server()}}) -- so +core never distinguishes its demand from any other owner's, and a consumer claiming a block cannot park what the front-end is showing. Because claims carry no state change, they are also the one part of a payload a locked board still accepts. diff --git a/tests/testthat/test-plugin-code.R b/tests/testthat/test-plugin-code.R index 4641c69c..9854a4a8 100644 --- a/tests/testthat/test-plugin-code.R +++ b/tests/testthat/test-plugin-code.R @@ -158,10 +158,7 @@ test_that("show code builds the board without evaluating or gating it", { testServer( get_s3_method("board_server", board), { - vis$gate("front-end") - board_update( - list(sustain = list(`front-end` = list(set = "a")), construct = "b") - ) + board_update(list(construct = "b")) vis$visible[["a"]](TRUE) session$flushReact() @@ -190,7 +187,11 @@ test_that("show code builds the board without evaluating or gating it", { expect_identical(rv$claims(), 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(...) gate_claim("front-end", "a") + ) ) }) @@ -214,11 +215,6 @@ test_that("show code requires the whole board, gating export on config", { testServer( get_s3_method("board_server", board), { - vis$gate("front-end") - board_update( - list(sustain = list(`front-end` = list(set = c("a", "b")))) - ) - for (id in c("a", "b")) { vis$visible[[id]](TRUE) } @@ -246,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(...) gate_claim("front-end", c("a", "b")) + ) ) out diff --git a/tests/testthat/test-visibility-gating.R b/tests/testthat/test-visibility-gating.R index 87c7a48f..6006b80e 100644 --- a/tests/testthat/test-visibility-gating.R +++ b/tests/testthat/test-visibility-gating.R @@ -191,20 +191,19 @@ constructed <- function(id) { id %in% probe_construct$ids } -# The front-end under test: it declares itself the gating owner and states its -# demand as a `sustain` claim under that label, exactly as any other consumer -# would. Each helper writes the payload channel once, since a second write -# before the next flush would clobber the first. +# The front-end under test: its callback declares itself the gating owner by +# returning its opening claim, and it states demand from then on as a `sustain` +# claim 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" claim <- function(...) { set_names(list(list(...)), front_end) } -gate_blocks <- function(vis, update, ...) { - - vis$gate(front_end) - require_blocks(update, ...) +gate_blocks <- function(...) { + gate_claim(front_end, c(...)) } require_blocks <- function(update, ...) { @@ -338,9 +337,9 @@ test_that("a producer gates evaluation and rendering on visibility", { args = list( x = board, plugins = list(), - callbacks = function(visibility, update, ...) { - gate_blocks(visibility, update, "b") + callbacks = function(visibility, ...) { render_blocks(visibility, "b") + gate_blocks("b") } ) ) @@ -377,9 +376,9 @@ test_that("the gate_visibility option disables gating", { args = list( x = board, plugins = list(), - callbacks = function(visibility, update, ...) { - gate_blocks(visibility, update, "b") + callbacks = function(visibility, ...) { render_blocks(visibility, "b") + gate_blocks("b") } ) ) @@ -428,9 +427,9 @@ test_that("a link change re-routes the pulled upstream", { args = list( x = board, plugins = list(), - callbacks = function(visibility, update, ...) { - gate_blocks(visibility, update, "b") + callbacks = function(visibility, ...) { render_blocks(visibility, "b") + gate_blocks("b") } ) ) @@ -477,9 +476,9 @@ test_that("a needed round trip with unchanged inputs does not re-evaluate", { args = list( x = board, plugins = list(), - callbacks = function(visibility, update, ...) { - gate_blocks(visibility, update, "b") + callbacks = function(visibility, ...) { render_blocks(visibility, "b") + gate_blocks("b") } ) ) @@ -543,9 +542,9 @@ test_that("a dormant block reports stale when an upstream re-evaluates", { args = list( x = board, plugins = list(), - callbacks = function(visibility, update, ...) { - gate_blocks(visibility, update, "a", "r") + callbacks = function(visibility, ...) { render_blocks(visibility, "a", "r") + gate_blocks("a", "r") } ) ) @@ -588,9 +587,9 @@ test_that("a dormant block whose upstreams are unchanged stays dormant", { args = list( x = board, plugins = list(), - callbacks = function(visibility, update, ...) { - gate_blocks(visibility, update, "a", "r") + callbacks = function(visibility, ...) { render_blocks(visibility, "a", "r") + gate_blocks("a", "r") } ) ) @@ -650,9 +649,9 @@ test_that("staleness propagates to the whole dormant downstream cone", { args = list( x = board, plugins = list(), - callbacks = function(visibility, update, ...) { - gate_blocks(visibility, update, "a", "b", "r") + callbacks = function(visibility, ...) { render_blocks(visibility, "a", "b", "r") + gate_blocks("a", "b", "r") } ) ) @@ -703,9 +702,9 @@ test_that("re-routing a dormant block's input marks it stale", { args = list( x = board, plugins = list(), - callbacks = function(visibility, update, ...) { - gate_blocks(visibility, update, "a", "b", "r") + callbacks = function(visibility, ...) { render_blocks(visibility, "a", "b", "r") + gate_blocks("a", "b", "r") } ) ) @@ -760,9 +759,9 @@ test_that("a stale block that re-evaluates is dormant when parked again", { args = list( x = board, plugins = list(), - callbacks = function(visibility, update, ...) { - gate_blocks(visibility, update, "a", "b", "r") + callbacks = function(visibility, ...) { render_blocks(visibility, "a", "b", "r") + gate_blocks("a", "b", "r") } ) ) @@ -839,8 +838,8 @@ test_that("an evaluation request brings a stale block current", { plugins = list(), callbacks = function(visibility, update, ...) { upd_channel <<- update - gate_blocks(visibility, update, "s1", "s2", "a", "r") render_blocks(visibility, "s1", "s2", "a", "r") + gate_blocks("s1", "s2", "a", "r") } ) ) @@ -907,9 +906,9 @@ test_that("an evaluation request evaluates a block edited while dormant", { args = list( x = board, plugins = list(), - callbacks = function(visibility, update, ...) { - gate_blocks(visibility, update, "s", "r") + callbacks = function(visibility, ...) { render_blocks(visibility, "s", "r") + gate_blocks("s", "r") } ) ) @@ -951,9 +950,9 @@ test_that("an edit and a request in one payload evaluate the edit", { args = list( x = board, plugins = list(), - callbacks = function(visibility, update, ...) { - gate_blocks(visibility, update, "s", "r") + callbacks = function(visibility, ...) { render_blocks(visibility, "s", "r") + gate_blocks("s", "r") } ) ) @@ -1001,9 +1000,9 @@ test_that("an evaluation request builds the blocks it needs", { args = list( x = board, plugins = list(), - callbacks = function(visibility, update, ...) { - gate_blocks(visibility, update, "s") + callbacks = function(visibility, ...) { render_blocks(visibility, "s") + gate_blocks("s") } ) ) @@ -1058,9 +1057,9 @@ test_that("a required claim holds a block until it is released", { args = list( x = board, plugins = list(), - callbacks = function(visibility, update, ...) { - gate_blocks(visibility, update, "s", "r") + callbacks = function(visibility, ...) { render_blocks(visibility, "s", "r") + gate_blocks("s", "r") } ) ) @@ -1118,9 +1117,9 @@ test_that("one owner's release leaves another owner's claim standing", { args = list( x = board, plugins = list(), - callbacks = function(visibility, update, ...) { - gate_blocks(visibility, update, "s", "r") + callbacks = function(visibility, ...) { render_blocks(visibility, "s", "r") + gate_blocks("s", "r") } ) ) @@ -1203,9 +1202,9 @@ test_that("a consumer cannot release what the front-end holds", { args = list( x = board, plugins = list(), - callbacks = function(visibility, update, ...) { - gate_blocks(visibility, update, "r") + callbacks = function(visibility, ...) { render_blocks(visibility, "r") + gate_blocks("r") } ) ) @@ -1259,9 +1258,9 @@ test_that("removing a claimed block prunes it from every owner", { args = list( x = board, plugins = list(), - callbacks = function(visibility, update, ...) { - gate_blocks(visibility, update, "s") + callbacks = function(visibility, ...) { render_blocks(visibility, "s") + gate_blocks("s") } ) ) @@ -1300,9 +1299,9 @@ test_that("a request for a block added in the same payload is honoured", { args = list( x = board, plugins = list(), - callbacks = function(visibility, update, ...) { - gate_blocks(visibility, update, "s") + callbacks = function(visibility, ...) { render_blocks(visibility, "s") + gate_blocks("s") } ) ) @@ -1353,9 +1352,9 @@ test_that("a construction request builds a block without evaluating it", { args = list( x = board, plugins = list(), - callbacks = function(visibility, update, ...) { - gate_blocks(visibility, update, "s") + callbacks = function(visibility, ...) { render_blocks(visibility, "s") + gate_blocks("s") } ) ) @@ -1406,9 +1405,9 @@ test_that("overlapping requests union rather than clash", { args = list( x = board, plugins = list(), - callbacks = function(visibility, update, ...) { - gate_blocks(visibility, update, "s") + callbacks = function(visibility, ...) { render_blocks(visibility, "s") + gate_blocks("s") } ) ) @@ -1461,9 +1460,9 @@ test_that("a request naming an unknown block is rejected", { args = list( x = board, plugins = list(), - callbacks = function(visibility, update, ...) { - gate_blocks(visibility, update, "a") + callbacks = function(visibility, ...) { render_blocks(visibility, "a") + gate_blocks("a") } ) ) @@ -1515,9 +1514,9 @@ test_that("a view switch does not re-evaluate shared upstream left needed", { args = list( x = board, plugins = list(), - callbacks = function(visibility, update, ...) { - gate_blocks(visibility, update, "t1") + callbacks = function(visibility, ...) { render_blocks(visibility, "t1") + gate_blocks("t1") } ) ) @@ -1566,9 +1565,9 @@ test_that("a variadic block skips re-evaluation on unchanged inputs", { args = list( x = board, plugins = list(), - callbacks = function(visibility, update, ...) { - gate_blocks(visibility, update, "v") + callbacks = function(visibility, ...) { render_blocks(visibility, "v") + gate_blocks("v") } ) ) @@ -1602,9 +1601,9 @@ test_that("an off-screen data-observing block does not pull its upstream", { args = list( x = board, plugins = list(), - callbacks = function(visibility, update, ...) { - gate_blocks(visibility, update, "c") + callbacks = function(visibility, ...) { render_blocks(visibility, "c") + gate_blocks("c") } ) ) @@ -1648,9 +1647,9 @@ test_that("an unrelated structural edit does not re-evaluate needed blocks", { args = list( x = board, plugins = list(), - callbacks = function(visibility, update, ...) { - gate_blocks(visibility, update, "b") + callbacks = function(visibility, ...) { render_blocks(visibility, "b") + gate_blocks("b") } ) ) @@ -1692,9 +1691,9 @@ test_that("adding a block does not re-evaluate existing needed blocks", { args = list( x = board, plugins = list(), - callbacks = function(visibility, update, ...) { - gate_blocks(visibility, update, "b") + callbacks = function(visibility, ...) { render_blocks(visibility, "b") + gate_blocks("b") } ) ) @@ -1727,9 +1726,9 @@ test_that("a variadic block receives its inputs as values, not reactives", { args = list( x = board, plugins = list(), - callbacks = function(visibility, update, ...) { - gate_blocks(visibility, update, "c") + callbacks = function(visibility, ...) { render_blocks(visibility, "c") + gate_blocks("c") } ) ) @@ -1765,9 +1764,9 @@ test_that("an off-screen variadic block does not pull its inputs", { args = list( x = board, plugins = list(), - callbacks = function(visibility, update, ...) { - gate_blocks(visibility, update, "e") + callbacks = function(visibility, ...) { render_blocks(visibility, "e") + gate_blocks("e") } ) ) @@ -1789,9 +1788,9 @@ ordered_board <- function() { ) } -visible_b <- function(visibility, update, ...) { - gate_blocks(visibility, update, "b") +visible_b <- function(visibility, ...) { render_blocks(visibility, "b") + gate_blocks("b") } test_that("the priority lane builds the needed set ahead of the backlog", { @@ -1816,9 +1815,9 @@ test_that("the priority lane builds the needed set ahead of the backlog", { args = list( x = ordered_board(), plugins = list(), - callbacks = function(visibility, update, ...) { - gate_blocks(visibility, update, "c") + callbacks = function(visibility, ...) { render_blocks(visibility, "c") + gate_blocks("c") } ) ) @@ -1849,8 +1848,8 @@ test_that("opening a view pulls its blocks ahead of a gated backlog", { args = list( x = ordered_board(), plugins = list(), - callbacks = function(visibility, update, ...) { - gate_blocks(visibility, update, "b") + callbacks = function(...) { + gate_blocks("b") } ) ) @@ -2007,10 +2006,7 @@ test_that("gate_fulfilled tracks the gating owner's claim alone", { ) add_vis_slots(vis, c("a", "b", "c")) - rv <- reactiveValues( - claims = reactiveVal(list(dock = c("a", "b"))), - gate_claimed = reactiveVal(TRUE) - ) + rv <- reactiveValues(claims = reactiveVal(list(dock = c("a", "b")))) vis$visible[["a"]](TRUE) vis$visible[["b"]](TRUE) @@ -2030,6 +2026,130 @@ test_that("gate_fulfilled tracks the gating owner's claim alone", { }) }) +test_that("a declared opening claim is in place before the first flush", { + + reset_probes() + + local_mocked_bindings(schedule_construction = drive_construction) + + claims_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 claim and its upstream are built ahead of the backlog, + # and nothing outside the claim evaluates. + expect_identical(claims_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") + gate_blocks("b") + }, + function(board, ...) { + observe(claims_at_first_flush <<- board$claims(), 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$claims(), 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_gate_claim))) + }, + args = list( + x = ordered_board(), + plugins = list(), + callbacks = function(...) list(extra = 42, gate_blocks("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(...) gate_claim("one", "b"), + function(...) gate_claim("two", "c") + ) + ) + ), + class = "gate_claim_ambiguous" + ) +}) + +test_that("a declared opening claim is validated as any claim is", { + + expect_error( + testServer( + get_s3_method("board_server", ordered_board()), + session$flushReact(), + args = list( + x = ordered_board(), + plugins = list(), + callbacks = function(...) gate_claim("front-end", "nope") + ) + ), + class = "board_update_sustain_unknown_id" + ) + + expect_error(gate_claim(""), class = "gate_claim_owner_invalid") + expect_error(gate_claim(NA_character_), class = "gate_claim_owner_invalid") + expect_error(gate_claim("fe", 1L), class = "gate_claim_blocks_invalid") +}) + test_that("the background waits for the front-end's rendered report", { reset_probes() @@ -2056,8 +2176,8 @@ test_that("the background waits for the front-end's rendered report", { args = list( x = ordered_board(), plugins = list(), - callbacks = function(visibility, update, ...) { - gate_blocks(visibility, update, "b") + callbacks = function(...) { + gate_blocks("b") } ) ) From 719bd94e39fddc64b5719f005abe1176e476859f Mon Sep 17 00:00:00 2001 From: Nicolas Bennett <3158446+nbenn@users.noreply.github.com> Date: Mon, 28 Sep 2026 14:42:31 +0000 Subject: [PATCH 5/6] Drop the typedjson remote now that it is on CRAN Core calls only json_read() and json_write_str(), both in the CRAN release, and resolving the GitHub remote was the one step in dependency install that needed the GitHub API. --- DESCRIPTION | 2 -- 1 file changed, 2 deletions(-) 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 = From 1ce531a1c50d59adbf2763594e22d335f8ae1a0d Mon Sep 17 00:00:00 2001 From: Nicolas Bennett <3158446+nbenn@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:39:08 +0000 Subject: [PATCH 6/6] Call the blocks a front-end keeps evaluated eager A board is eager by default and evaluates every block; a front-end makes it lazy by returning eager(owner, blocks) from its callback, and the blocks any owner holds eager are what a lazy board evaluates. The same word now names the payload component, so the `sustain` component becomes `eager` and gate_claim() becomes eager(), and the docs stop describing the declaration as driving visibility. Nothing here has been released, so the rename carries no deprecation. --- NAMESPACE | 2 +- NEWS.md | 98 ++++----- R/block-server.R | 102 +++++----- R/board-server.R | 248 +++++++++++----------- R/stack-gate.R | 14 +- inst/examples/board/code/app.R | 2 +- man/block_server.Rd | 106 +++++----- man/board_server.Rd | 106 +++++----- man/board_update.Rd | 12 +- tests/testthat/test-board-lock.R | 8 +- tests/testthat/test-board-server.R | 42 ++-- tests/testthat/test-plugin-code.R | 12 +- tests/testthat/test-visibility-gating.R | 260 ++++++++++++------------ 13 files changed, 509 insertions(+), 503 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 9dc9f5e3..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) @@ -462,7 +463,6 @@ export(external_ctrl_vars) export(fatal_log_level) export(filebrowser_volumes) export(forward_ctor) -export(gate_claim) export(gate_stacks) export(generate_code) export(generate_code_server) diff --git a/NEWS.md b/NEWS.md index a17f46d4..fb89e0ee 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,21 +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: what it needs evaluated is - a `sustain` claim held 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 writes another's claim, the channel cannot silently become - multi-writer the way `required` did. Whether a board gates is now declared - rather than inferred from whether anything had written demand: a front-end's - callback returns `gate_claim(owner, blocks)`, and core seeds `blocks` as that - owner's claim 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. A board whose callbacks declare nothing - is not gated, so a consumer claiming one block cannot park every other block - on it. 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). + 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 @@ -70,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 @@ -88,26 +87,27 @@ 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 claims the blocks of every open stack plus every unstacked block 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)`. 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 gate declaration, so it cannot + 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 @@ -201,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 @@ -293,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 e127db00..d9f208ba 100644 --- a/R/block-server.R +++ b/R/block-server.R @@ -58,47 +58,46 @@ #' [block_output()] generic. The [block_ui()] generic can then be used to #' control rendering of outputs. #' -#' A front-end (such as blockr.dock) declares that it will drive visibility by -#' returning a [gate_claim()] from the callback it registers with -#' [board_server()], naming its owner label and the blocks it needs evaluated -#' from the start. That declaration, and nothing else, is what flips the board -#' from evaluating everything to evaluating only what is needed; a board whose -#' callbacks declare nothing has every block needed and behaves as it always -#' has, and the `gate_visibility` [blockr_option()] (default `TRUE`) turns -#' gating off entirely. Core reads the declaration as it runs the callbacks and -#' seeds the opening claim 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. +#' 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 is a `sustain` claim held 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()]). +#' 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 is gated on the *needed* set, the claimed blocks together with -#' their upstream closure over [board_links()] (recomputed only when claims or -#' links change). A block's input data reactives stay unfulfilled (they +#' 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 -#' claimed nor feeding a claimed 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 claimed block) evaluates but does not -#' render. +#' 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 is gated on `visible`, the per-block channel through which the -#' front-end reports what it has painted -- the effect, where a claim 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. +#' 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 claimed 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 -#' block it claims 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 +#' 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. @@ -108,19 +107,19 @@ #' render as a [bslib::accordion()] which opens one stack and collapses the #' rest (see [stack_ui()]), so on a stacked board part of what is on screen is #' hidden from the first render and any stack can be collapsed afterwards. -#' The `gate_stacks()` callback reads that accordion back, claiming the blocks -#' of every open stack plus every unstacked block 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 its opening claim: a board with no gate -#' declared is one where every block is needed, and a collapsed stack's blocks -#' would otherwise evaluate once before the accordion reports. The accordion's -#' report then refines the claim 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 @@ -161,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 `gate` `reactiveVal` naming -#' the front-end driving visibility, plus `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, diff --git a/R/board-server.R b/R/board-server.R index 56055b5a..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,26 +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. #' -#' A claim asks for evaluation and nothing else: nothing about what is on -#' screen changes. The front-end is an owner like any other -- it holds a claim -#' under the label it declared with [gate_claim()] (see [block_server()]) -- so -#' core never distinguishes its demand from any other owner's, and a consumer -#' claiming a block cannot park what the front-end is showing. Because claims -#' 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. @@ -86,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 gate declaration, so it cannot turn a lazily +#' 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 @@ -115,32 +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. A callback that drives visibility declares itself the -#' gating front-end by returning `gate_claim(owner, blocks)`, on its own or as -#' one element of a list whose other elements are passed on to plugins as -#' usual; core seeds `blocks` as that owner's claim before the first flush, and -#' at most one callback may declare. With no declaration every block is needed. -#' What the front-end needs evaluated from then on travels as a `sustain` claim -#' 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 gates -#' rendering on it, and holds background construction until every claimed block -#' is reported painted. 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. +#' 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 it declares the initially -#' open stacks as its opening claim and reads that input only to refine it. +#' 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 @@ -214,13 +216,13 @@ 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. The front-end is one such owner. + # 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( { @@ -801,11 +803,11 @@ block_frozen <- function(id, vis) { isTRUE(vis$frozen[[id]]()) } -# Only the gating front-end's own claim is compared against paint: a claim held -# by anyone else names blocks nobody is putting on screen, and holding the -# backlog for one would stall it for the rest of the session. +# 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$claims()[[vis$gate()]], block_visible, vis)) + all(lgl_ply(rv$eager_blocks()[[vis$gate()]], block_visible, vis)) } validate_vis <- function(vis) { @@ -842,102 +844,102 @@ valid_gate <- function(x) { is.null(x) || (is_string(x) && !is.na(x) && nzchar(x)) } -#' @param owner Label under which the gating front-end holds its claim, as it -#' would name itself in a `sustain` component +#' @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 -gate_claim <- function(owner, blocks = character()) { +eager <- function(owner, blocks = character()) { if (is.null(owner) || !valid_gate(owner)) { blockr_abort( - "Expecting a gate claim owner to be a nonempty string.", - class = "gate_claim_owner_invalid" + "Expecting the owner of eager blocks to be a nonempty string.", + class = "eager_owner_invalid" ) } if (!is.character(blocks)) { blockr_abort( - "Expecting a gate claim to name its blocks as a character vector.", - class = "gate_claim_blocks_invalid" + "Expecting eager blocks to be named by a character vector.", + class = "eager_blocks_invalid" ) } - structure(list(owner = owner, blocks = blocks), class = "gate_claim") + structure(list(owner = owner, blocks = blocks), class = "eager_blocks") } -is_gate_claim <- function(x) { - inherits(x, "gate_claim") +is_eager_blocks <- function(x) { + inherits(x, "eager_blocks") } run_callbacks <- function(callbacks, args, rv, vis) { res <- lapply(callbacks, do.call, args) - seed_gate_claim(res, rv, vis) + seed_eager_blocks(res, rv, vis) - Filter(Negate(is.null), lapply(res, drop_gate_claim)) + 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_gate_claims <- function(res) { +callback_eager_blocks <- function(res) { - if (is_gate_claim(res)) { + if (is_eager_blocks(res)) { return(list(res)) } if (is.list(res) && !is.object(res)) { - return(Filter(is_gate_claim, res)) + return(Filter(is_eager_blocks, res)) } list() } -drop_gate_claim <- function(res) { +drop_eager_blocks <- function(res) { - if (is_gate_claim(res)) { + if (is_eager_blocks(res)) { return(NULL) } if (is.list(res) && !is.object(res)) { - return(Filter(Negate(is_gate_claim), res)) + return(Filter(Negate(is_eager_blocks), res)) } res } -seed_gate_claim <- function(res, rv, vis) { +seed_eager_blocks <- function(res, rv, vis) { - claims <- do.call(c, lapply(res, callback_gate_claims)) + declared <- do.call(c, lapply(res, callback_eager_blocks)) - if (!length(claims)) { + if (!length(declared)) { return(invisible()) } - if (length(claims) > 1L) { + if (length(declared) > 1L) { blockr_abort( - "Expecting at most one callback to declare itself the gating ", - "front-end, but {length(claims)} did: {chr_xtr(claims, 'owner')}.", - class = "gate_claim_ambiguous" + "Expecting at most one callback to return eager blocks, but ", + "{length(declared)} did: {chr_xtr(declared, 'owner')}.", + class = "eager_declaration_ambiguous" ) } - claim <- claims[[1L]] + decl <- declared[[1L]] - validate_claim_delta( - list(set = claim$blocks), - claim$owner, + validate_eager_delta( + list(set = decl$blocks), + decl$owner, isolate(board_block_ids(rv$board)) ) - vis$gate(claim$owner) + 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$claims(filter_empty(set_names(list(claim$blocks), claim$owner))) + rv$eager_blocks(filter_empty(set_names(list(decl$blocks), decl$owner))) ) invisible() @@ -952,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 @@ -973,7 +975,7 @@ id_request_components <- function() { } update_request_components <- function() { - c(id_request_components(), "sustain") + c(id_request_components(), "eager") } needed_block_ids <- function(rv) { @@ -1139,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() @@ -1421,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 @@ -1440,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 @@ -1630,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) } } @@ -1671,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" ) } @@ -1706,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" ) } @@ -1725,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" ) } @@ -2329,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"]])) { @@ -2352,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 c1f034ea..5ace7fdd 100644 --- a/R/stack-gate.R +++ b/R/stack-gate.R @@ -6,20 +6,20 @@ gate_stacks <- function() { observe(show_open_stacks(board, visibility, update, session)) - open_stacks_claim(isolate(board$board), session) + open_stacks_eager(isolate(board$board), session) } } # A stackless board renders an accordion that never binds as an input, so -# nothing would ever arrive to refine a claim made on its behalf and it would -# stay parked for the session; it is left ungated instead. -open_stacks_claim <- function(board, session) { +# 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) { if (!has_length(board_stack_ids(board))) { return(NULL) } - gate_claim( + eager( stack_gate_owner(session), shown_block_ids(board, default_open_stacks(board_stacks(board))) ) @@ -30,7 +30,7 @@ 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 claim the callback declared -- + # 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)) { @@ -42,7 +42,7 @@ show_open_stacks <- function(board, vis, update, session) { shown <- shown_block_ids(brd, open_stack_ids(open, brd, session)) - update(list(sustain = set_names(list(list(set = shown)), owner))) + update(list(eager = set_names(list(list(set = shown)), owner))) for (id in ls(vis$visible)) { vis$visible[[id]](id %in% shown) diff --git a/inst/examples/board/code/app.R b/inst/examples/board/code/app.R index f54931f9..028bf672 100644 --- a/inst/examples/board/code/app.R +++ b/inst/examples/board/code/app.R @@ -19,6 +19,6 @@ serve( status_b = reval_if(board$eval[["b"]]) ) - gate_claim("front-end", "a") + eager("front-end", "a") } ) diff --git a/man/block_server.Rd b/man/block_server.Rd index 2942cc04..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 \code{gate} \code{reactiveVal} naming -the front-end driving visibility, plus \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,47 +144,46 @@ 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) declares that it will drive visibility by -returning a \code{\link[=gate_claim]{gate_claim()}} 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. That declaration, and nothing else, is what flips the board -from evaluating everything to evaluating only what is needed; a board whose -callbacks declare nothing has every block needed and behaves as it always -has, and the \code{gate_visibility} \code{\link[=blockr_option]{blockr_option()}} (default \code{TRUE}) turns -gating off entirely. Core reads the declaration as it runs the callbacks and -seeds the opening claim 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. +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 is a \code{sustain} claim held 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 is gated on the \emph{needed} set, the claimed blocks together with -their upstream closure over \code{\link[=board_links]{board_links()}} (recomputed only when claims or -links change). A block's input data reactives stay unfulfilled (they +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 -claimed nor feeding a claimed 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 claimed block) evaluates but does not -render. - -Rendering is gated on \code{visible}, the per-block channel through which the -front-end reports what it has painted -- the effect, where a claim 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. +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 claimed 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 -block it claims 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 +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. @@ -193,19 +193,19 @@ 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. -The \code{gate_stacks()} callback reads that accordion back, claiming the blocks -of every open stack plus every unstacked block 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 its opening claim: a board with no gate -declared is one where every block is needed, and a collapsed stack's blocks -would otherwise evaluate once before the accordion reports. The accordion's -report then refines the claim 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 1eba44b5..6e30e333 100644 --- a/man/board_server.Rd +++ b/man/board_server.Rd @@ -3,7 +3,7 @@ \name{board_server} \alias{board_server} \alias{board_server.board} -\alias{gate_claim} +\alias{eager} \alias{gate_stacks} \title{Board server} \usage{ @@ -19,7 +19,7 @@ board_server(id, x, ...) ... ) -gate_claim(owner, blocks = character()) +eager(owner, blocks = character()) gate_stacks() } @@ -36,38 +36,40 @@ gate_stacks() and registry sourced options)} \item{callbacks}{Single (or list of) callback function(s) registering -additional observers. A callback that drives visibility declares itself the -gating front-end by returning \code{gate_claim(owner, blocks)}, on its own or as -one element of a list whose other elements are passed on to plugins as -usual; core seeds \code{blocks} as that owner's claim before the first flush, and -at most one callback may declare. With no declaration every block is needed. -What the front-end needs evaluated from then on travels as a \code{sustain} claim -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 gates -rendering on it, and holds background construction until every claimed block -is reported painted. 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. +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 it declares the initially -open stacks as its opening claim and reads that input only to refine it.} +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 gating front-end holds its claim, as it -would name itself in a \code{sustain} component} +\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} } @@ -101,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") ) @@ -124,26 +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. - -A claim asks for evaluation and nothing else: nothing about what is on -screen changes. The front-end is an owner like any other -- it holds a claim -under the label it declared with \code{\link[=gate_claim]{gate_claim()}} (see \code{\link[=block_server]{block_server()}}) -- so -core never distinguishes its demand from any other owner's, and a consumer -claiming a block cannot park what the front-end is showing. Because claims -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. @@ -165,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 gate declaration, so it cannot turn a lazily +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-plugin-code.R b/tests/testthat/test-plugin-code.R index 9854a4a8..c4b32007 100644 --- a/tests/testthat/test-plugin-code.R +++ b/tests/testthat/test-plugin-code.R @@ -174,23 +174,23 @@ 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") - # The front-end's claim is left exactly as it was, and the export adds + # The front-end's eager set is left exactly as it was, and the export adds # none of its own - expect_identical(rv$claims(), list(`front-end` = "a")) + 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 beyond - # the front-end's own claim + # the front-end's own eager set expect_length(rv$evaluating(), 0L) - expect_identical(rv$claims(), list(`front-end` = "a")) + 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"), - callbacks = function(...) gate_claim("front-end", "a") + callbacks = function(...) eager("front-end", "a") ) ) }) @@ -245,7 +245,7 @@ test_that("show code requires the whole board, gating export on config", { args = list( x = board, plugins = board_plugins(board, "generate_code"), - callbacks = function(...) gate_claim("front-end", c("a", "b")) + callbacks = function(...) eager("front-end", c("a", "b")) ) ) diff --git a/tests/testthat/test-visibility-gating.R b/tests/testthat/test-visibility-gating.R index 6006b80e..fc241267 100644 --- a/tests/testthat/test-visibility-gating.R +++ b/tests/testthat/test-visibility-gating.R @@ -191,31 +191,31 @@ constructed <- function(id) { id %in% probe_construct$ids } -# The front-end under test: its callback declares itself the gating owner by -# returning its opening claim, and it states demand from then on as a `sustain` -# claim 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. +# 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" -claim <- function(...) { +front_delta <- function(...) { set_names(list(list(...)), front_end) } -gate_blocks <- function(...) { - gate_claim(front_end, c(...)) +declare_eager <- function(...) { + eager(front_end, c(...)) } require_blocks <- function(update, ...) { - update(list(sustain = claim(add = c(...)))) + update(list(eager = front_delta(add = c(...)))) invisible() } release_blocks <- function(update, ...) { - update(list(sustain = claim(rm = c(...)))) + update(list(eager = front_delta(rm = c(...)))) invisible() } @@ -240,15 +240,15 @@ park_blocks <- function(update, vis, ...) { invisible() } -claimed <- function(rv) { - rv$claims()[[front_end]] +front_eager <- function(rv) { + rv$eager_blocks()[[front_end]] } -# The front-end holds a claim like any other owner, so a test about what +# 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_claims <- function(rv) { - claims <- rv$claims() - claims[setdiff(names(claims), front_end)] +consumer_eager <- function(rv) { + held <- rv$eager_blocks() + held[setdiff(names(held), front_end)] } block_conditions <- function(rv, id, severity) { @@ -310,7 +310,7 @@ test_that("a producer gates evaluation and rendering on visibility", { { session$flushReact() - expect_setequal(claimed(rv), "b") + expect_setequal(front_eager(rv), "b") expect_true(evaluated("b")) expect_true(rendered("b")) @@ -339,7 +339,7 @@ test_that("a producer gates evaluation and rendering on visibility", { plugins = list(), callbacks = function(visibility, ...) { render_blocks(visibility, "b") - gate_blocks("b") + declare_eager("b") } ) ) @@ -378,7 +378,7 @@ test_that("the gate_visibility option disables gating", { plugins = list(), callbacks = function(visibility, ...) { render_blocks(visibility, "b") - gate_blocks("b") + declare_eager("b") } ) ) @@ -429,7 +429,7 @@ test_that("a link change re-routes the pulled upstream", { plugins = list(), callbacks = function(visibility, ...) { render_blocks(visibility, "b") - gate_blocks("b") + declare_eager("b") } ) ) @@ -478,7 +478,7 @@ test_that("a needed round trip with unchanged inputs does not re-evaluate", { plugins = list(), callbacks = function(visibility, ...) { render_blocks(visibility, "b") - gate_blocks("b") + declare_eager("b") } ) ) @@ -544,7 +544,7 @@ test_that("a dormant block reports stale when an upstream re-evaluates", { plugins = list(), callbacks = function(visibility, ...) { render_blocks(visibility, "a", "r") - gate_blocks("a", "r") + declare_eager("a", "r") } ) ) @@ -589,7 +589,7 @@ test_that("a dormant block whose upstreams are unchanged stays dormant", { plugins = list(), callbacks = function(visibility, ...) { render_blocks(visibility, "a", "r") - gate_blocks("a", "r") + declare_eager("a", "r") } ) ) @@ -651,7 +651,7 @@ test_that("staleness propagates to the whole dormant downstream cone", { plugins = list(), callbacks = function(visibility, ...) { render_blocks(visibility, "a", "b", "r") - gate_blocks("a", "b", "r") + declare_eager("a", "b", "r") } ) ) @@ -704,7 +704,7 @@ test_that("re-routing a dormant block's input marks it stale", { plugins = list(), callbacks = function(visibility, ...) { render_blocks(visibility, "a", "b", "r") - gate_blocks("a", "b", "r") + declare_eager("a", "b", "r") } ) ) @@ -761,7 +761,7 @@ test_that("a stale block that re-evaluates is dormant when parked again", { plugins = list(), callbacks = function(visibility, ...) { render_blocks(visibility, "a", "b", "r") - gate_blocks("a", "b", "r") + declare_eager("a", "b", "r") } ) ) @@ -829,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("r" %in% claimed(rv)) + expect_false("r" %in% front_eager(rv)) expect_false(vis$visible[["r"]]()) expect_false(rendered("r")) }, @@ -839,7 +839,7 @@ test_that("an evaluation request brings a stale block current", { callbacks = function(visibility, update, ...) { upd_channel <<- update render_blocks(visibility, "s1", "s2", "a", "r") - gate_blocks("s1", "s2", "a", "r") + declare_eager("s1", "s2", "a", "r") } ) ) @@ -908,7 +908,7 @@ test_that("an evaluation request evaluates a block edited while dormant", { plugins = list(), callbacks = function(visibility, ...) { render_blocks(visibility, "s", "r") - gate_blocks("s", "r") + declare_eager("s", "r") } ) ) @@ -952,7 +952,7 @@ test_that("an edit and a request in one payload evaluate the edit", { plugins = list(), callbacks = function(visibility, ...) { render_blocks(visibility, "s", "r") - gate_blocks("s", "r") + declare_eager("s", "r") } ) ) @@ -1002,13 +1002,13 @@ test_that("an evaluation request builds the blocks it needs", { plugins = list(), callbacks = function(visibility, ...) { render_blocks(visibility, "s") - gate_blocks("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() @@ -1034,8 +1034,8 @@ test_that("a required claim holds a block until it is released", { 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") @@ -1043,15 +1043,15 @@ 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(consumer_claims(rv), 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(consumer_claims(rv), 0L) + expect_length(consumer_eager(rv), 0L) expect_false(rendered("r")) }, args = list( @@ -1059,13 +1059,13 @@ test_that("a required claim holds a block until it is released", { plugins = list(), callbacks = function(visibility, ...) { render_blocks(visibility, "s", "r") - gate_blocks("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() @@ -1089,29 +1089,29 @@ test_that("one owner's release leaves another owner's claim standing", { 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(consumer_claims(rv), 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(consumer_claims(rv), 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(consumer_claims(rv), 0L) + expect_length(consumer_eager(rv), 0L) expect_identical(rv$eval[["r"]](), "dormant") }, args = list( @@ -1119,13 +1119,13 @@ test_that("one owner's release leaves another owner's claim standing", { plugins = list(), callbacks = function(visibility, ...) { render_blocks(visibility, "s", "r") - gate_blocks("s", "r") + declare_eager("s", "r") } ) ) }) -test_that("a consumer claim does not gate an ungated board", { +test_that("a consumer's eager block does not make an eager board lazy", { reset_probes() @@ -1148,13 +1148,13 @@ test_that("a consumer claim does not gate an ungated board", { { session$flushReact() - board_update(list(sustain = list(consumer = list(set = "a")))) + board_update(list(eager = list(consumer = list(set = "a")))) session$flushReact() - # Nothing declared a gate, so a claim is a no-op: it says what one - # consumer wants evaluated, never that everything else may be parked. - # Inferring the gate from claims instead would leave b -- which nobody - # asked for -- dormant and blank on a board that has no front-end. + # 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")) @@ -1182,20 +1182,20 @@ test_that("a consumer cannot release what the front-end holds", { { session$flushReact() - expect_identical(claimed(rv), "r") + 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(sustain = list(consumer = list(set = "r")))) + board_update(list(eager = list(consumer = list(set = "r")))) session$flushReact() - board_update(list(sustain = list(consumer = list(set = character())))) + board_update(list(eager = list(consumer = list(set = character())))) session$flushReact() - expect_identical(claimed(rv), "r") - expect_length(consumer_claims(rv), 0L) + 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") }, @@ -1204,13 +1204,13 @@ test_that("a consumer cannot release what the front-end holds", { plugins = list(), callbacks = function(visibility, ...) { render_blocks(visibility, "r") - gate_blocks("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() @@ -1231,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") ) @@ -1242,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(consumer_claims(rv), 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(consumer_claims(rv), list(one = "s")) + expect_identical(consumer_eager(rv), list(one = "s")) }, args = list( x = board, plugins = list(), callbacks = function(visibility, ...) { render_blocks(visibility, "s") - gate_blocks("s") + declare_eager("s") } ) ) @@ -1301,7 +1301,7 @@ test_that("a request for a block added in the same payload is honoured", { plugins = list(), callbacks = function(visibility, ...) { render_blocks(visibility, "s") - gate_blocks("s") + declare_eager("s") } ) ) @@ -1345,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 join the claim the front-end holds, which is what - # keeps it from parking what is on screen. - expect_identical(claimed(rv), "s") + # 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, ...) { render_blocks(visibility, "s") - gate_blocks("s") + declare_eager("s") } ) ) @@ -1383,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() @@ -1391,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(consumer_claims(rv), 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(consumer_claims(rv), list(one = "r")) + expect_identical(consumer_eager(rv), list(one = "r")) }, args = list( x = board, plugins = list(), callbacks = function(visibility, ...) { render_blocks(visibility, "s") - gate_blocks("s") + declare_eager("s") } ) ) @@ -1442,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(consumer_claims(rv), 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(consumer_claims(rv), 0L) + expect_length(consumer_eager(rv), 0L) expect_setequal(rv$needed(), "a") }, @@ -1462,7 +1462,7 @@ test_that("a request naming an unknown block is rejected", { plugins = list(), callbacks = function(visibility, ...) { render_blocks(visibility, "a") - gate_blocks("a") + declare_eager("a") } ) ) @@ -1503,7 +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. - board_update(list(sustain = claim(add = "t2", rm = "t1"))) + board_update(list(eager = front_delta(add = "t2", rm = "t1"))) render_blocks(vis, "t2") session$flushReact() @@ -1516,7 +1516,7 @@ test_that("a view switch does not re-evaluate shared upstream left needed", { plugins = list(), callbacks = function(visibility, ...) { render_blocks(visibility, "t1") - gate_blocks("t1") + declare_eager("t1") } ) ) @@ -1567,7 +1567,7 @@ test_that("a variadic block skips re-evaluation on unchanged inputs", { plugins = list(), callbacks = function(visibility, ...) { render_blocks(visibility, "v") - gate_blocks("v") + declare_eager("v") } ) ) @@ -1603,7 +1603,7 @@ test_that("an off-screen data-observing block does not pull its upstream", { plugins = list(), callbacks = function(visibility, ...) { render_blocks(visibility, "c") - gate_blocks("c") + declare_eager("c") } ) ) @@ -1649,7 +1649,7 @@ test_that("an unrelated structural edit does not re-evaluate needed blocks", { plugins = list(), callbacks = function(visibility, ...) { render_blocks(visibility, "b") - gate_blocks("b") + declare_eager("b") } ) ) @@ -1693,7 +1693,7 @@ test_that("adding a block does not re-evaluate existing needed blocks", { plugins = list(), callbacks = function(visibility, ...) { render_blocks(visibility, "b") - gate_blocks("b") + declare_eager("b") } ) ) @@ -1728,7 +1728,7 @@ test_that("a variadic block receives its inputs as values, not reactives", { plugins = list(), callbacks = function(visibility, ...) { render_blocks(visibility, "c") - gate_blocks("c") + declare_eager("c") } ) ) @@ -1766,7 +1766,7 @@ test_that("an off-screen variadic block does not pull its inputs", { plugins = list(), callbacks = function(visibility, ...) { render_blocks(visibility, "e") - gate_blocks("e") + declare_eager("e") } ) ) @@ -1790,7 +1790,7 @@ ordered_board <- function() { visible_b <- function(visibility, ...) { render_blocks(visibility, "b") - gate_blocks("b") + declare_eager("b") } test_that("the priority lane builds the needed set ahead of the backlog", { @@ -1817,7 +1817,7 @@ test_that("the priority lane builds the needed set ahead of the backlog", { plugins = list(), callbacks = function(visibility, ...) { render_blocks(visibility, "c") - gate_blocks("c") + declare_eager("c") } ) ) @@ -1849,7 +1849,7 @@ test_that("opening a view pulls its blocks ahead of a gated backlog", { x = ordered_board(), plugins = list(), callbacks = function(...) { - gate_blocks("b") + declare_eager("b") } ) ) @@ -1982,7 +1982,7 @@ test_that("validate_vis hard-errors on an off-contract slot", { }) }) -test_that("gating activates on the declaration, not on a claim", { +test_that("a board turns lazy on the declaration, not on an eager block", { isolate({ vis <- list(gate = reactiveVal(NULL)) @@ -1997,7 +1997,7 @@ test_that("gating activates on the declaration, not on a claim", { }) }) -test_that("gate_fulfilled tracks the gating owner's claim alone", { +test_that("gate_fulfilled tracks the front-end's eager set alone", { isolate({ vis <- list( @@ -2006,7 +2006,7 @@ test_that("gate_fulfilled tracks the gating owner's claim alone", { ) add_vis_slots(vis, c("a", "b", "c")) - rv <- reactiveValues(claims = reactiveVal(list(dock = c("a", "b")))) + rv <- reactiveValues(eager_blocks = reactiveVal(list(dock = c("a", "b")))) vis$visible[["a"]](TRUE) vis$visible[["b"]](TRUE) @@ -2015,24 +2015,24 @@ test_that("gate_fulfilled tracks the gating owner's claim alone", { vis$visible[["b"]](FALSE) expect_false(gate_fulfilled(vis, rv)) - # Another owner's claim on an off-screen block never lands on screen, so + # 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$claims(list(dock = c("a", "b"), consumer = "c")) + rv$eager_blocks(list(dock = c("a", "b"), consumer = "c")) expect_true(gate_fulfilled(vis, rv)) - rv$claims(list()) + rv$eager_blocks(list()) expect_true(gate_fulfilled(vis, rv)) }) }) -test_that("a declared opening claim is in place before the first flush", { +test_that("a declared eager set is in place before the first flush", { reset_probes() local_mocked_bindings(schedule_construction = drive_construction) - claims_at_first_flush <- NULL + eager_at_first_flush <- NULL testServer( get_s3_method("board_server", ordered_board()), @@ -2040,9 +2040,9 @@ test_that("a declared opening claim is in place before the first flush", { session$flushReact() # Seeded as the callbacks run, so the first construction pass already - # has it: only the claim and its upstream are built ahead of the backlog, - # and nothing outside the claim evaluates. - expect_identical(claims_at_first_flush, list(`front-end` = "b")) + # 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")) @@ -2055,10 +2055,10 @@ test_that("a declared opening claim is in place before the first flush", { callbacks = list( function(visibility, ...) { render_blocks(visibility, "b") - gate_blocks("b") + declare_eager("b") }, function(board, ...) { - observe(claims_at_first_flush <<- board$claims(), priority = Inf) + observe(eager_at_first_flush <<- board$eager_blocks(), priority = Inf) NULL } ) @@ -2073,16 +2073,16 @@ test_that("a declaration travels alongside a callback's plugin arguments", { { session$flushReact() - expect_identical(rv$claims(), list(`front-end` = "b")) + 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_gate_claim))) + expect_false(any(lgl_ply(session$returned, is_eager_blocks))) }, args = list( x = ordered_board(), plugins = list(), - callbacks = function(...) list(extra = 42, gate_blocks("b")), + callbacks = function(...) list(extra = 42, declare_eager("b")), callback_location = "start" ) ) @@ -2121,16 +2121,16 @@ test_that("at most one callback declares itself the gating front-end", { x = ordered_board(), plugins = list(), callbacks = list( - function(...) gate_claim("one", "b"), - function(...) gate_claim("two", "c") + function(...) eager("one", "b"), + function(...) eager("two", "c") ) ) ), - class = "gate_claim_ambiguous" + class = "eager_declaration_ambiguous" ) }) -test_that("a declared opening claim is validated as any claim is", { +test_that("a declared eager set is validated as any eager delta is", { expect_error( testServer( @@ -2139,15 +2139,15 @@ test_that("a declared opening claim is validated as any claim is", { args = list( x = ordered_board(), plugins = list(), - callbacks = function(...) gate_claim("front-end", "nope") + callbacks = function(...) eager("front-end", "nope") ) ), - class = "board_update_sustain_unknown_id" + class = "board_update_eager_unknown_id" ) - expect_error(gate_claim(""), class = "gate_claim_owner_invalid") - expect_error(gate_claim(NA_character_), class = "gate_claim_owner_invalid") - expect_error(gate_claim("fe", 1L), class = "gate_claim_blocks_invalid") + 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", { @@ -2177,7 +2177,7 @@ test_that("the background waits for the front-end's rendered report", { x = ordered_board(), plugins = list(), callbacks = function(...) { - gate_blocks("b") + declare_eager("b") } ) ) @@ -2273,10 +2273,10 @@ 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: a claim like any other owner's, +# What core's own stack-gating callback holds eager, like any other owner, # under the label gate_stacks() takes from the board session. -stack_claim <- function(rv, session) { - rv$claims()[[stack_gate_owner(session)]] +stack_eager <- function(rv, session) { + rv$eager_blocks()[[stack_gate_owner(session)]] } report_open_stacks <- function(session, ...) { @@ -2305,7 +2305,7 @@ test_that("core requires the open stacks and every unstacked block", { # 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)) - expect_setequal(stack_claim(rv, session), c("a", "b", "e")) + expect_setequal(stack_eager(rv, session), c("a", "b", "e")) expect_false(evaluated("c")) expect_false(rendered("c")) @@ -2313,7 +2313,7 @@ test_that("core requires the open stacks and every unstacked block", { report_open_stacks(session, "s1") session$flushReact() - expect_setequal(stack_claim(rv, session), 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)) @@ -2352,7 +2352,7 @@ test_that("expanding a stack requires its blocks and collapsing parks them", { session$flushReact() expect_setequal( - stack_claim(rv, session), + stack_eager(rv, session), c("a", "b", "c", "d", "e") ) @@ -2362,7 +2362,7 @@ test_that("expanding a stack requires its blocks and collapsing parks them", { report_open_stacks(session, "s2") session$flushReact() - expect_setequal(stack_claim(rv, session), 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 @@ -2424,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(stack_claim(rv, session), c("a", "b", "e")) + expect_setequal(stack_eager(rv, session), c("a", "b", "e")) report_open_stacks(session) session$flushReact() - expect_setequal(stack_claim(rv, session), "e") + expect_setequal(stack_eager(rv, session), "e") expect_setequal(rv$needed(), "e") }, args = list(x = board, plugins = list()) @@ -2464,7 +2464,7 @@ test_that("a board without stacks requires every block", { report_open_stacks(session) session$flushReact() - expect_setequal(stack_claim(rv, session), c("a", "b")) + expect_setequal(stack_eager(rv, session), c("a", "b")) expect_true(evaluated("b")) expect_true(rendered("b"))