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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# blockr.dock (development version)

* The dock now states its evaluation demand as the blocks it holds eager,
in place of the per-block `required` channel blockr.core has retired.
Its board callback makes the board lazy by returning `eager()` with the
active view's front panels, which core seeds as the dock's eager set
before the first flush. From there, what the dock has on screen travels
as an `eager` update under the same owner label -- one payload per view
switch where the retired channel took a write per slot, and a card that
leaves the screen is released by its absence from the set rather than
by a second write. Requires blockr.core with `eager()` (#417).

* A card the dock has built but is not showing no longer carries
construction demand of its own. The retired `required` channel had a
third state for it, which paced those blocks into core's priority
construction lane; nothing replaces it, so the blocks behind an
unvisited tab are built by core's background pass in its own order and
fronting one holds it eager. First paint therefore waits on fewer blocks
than before (#417).

* A new `insert_block_action` puts a block into an existing link
([#459](https://github.com/BristolMyersSquibb/blockr.dock/issues/459)).
Triggered with a link id, it offers the same block browser as the add and
Expand Down
50 changes: 18 additions & 32 deletions R/block-ui.R
Original file line number Diff line number Diff line change
Expand Up @@ -137,15 +137,13 @@ insert_block_ui.dock_board <- function(id, x, blocks, dock, ...,
}

# The dock's build ledger is core's `visible` axis: a per-block reactiveVal,
# logical (NA never built / FALSE built off screen / TRUE painted now) like
# `required`, so `built_cards()` reads `!is.na(visible)`. It once read
# `required` non-NA, but that axis is a multi-writer construction-demand
# channel -- core's "Show code" marks every block required to export the whole
# script -- so a demand with no card masqueraded as built and blanked the view
# on its first visit. `visible` is written only where the dock builds, paints
# or reconciles a card. The dock still writes `required[[id]]` FALSE off screen
# / TRUE on screen (construction demand); block removal needs no dock write --
# core drops the slot, dropping the card from the ledger too.
# logical (NA never built / FALSE built off screen / TRUE painted now), so
# `built_cards()` reads `!is.na(visible)`. It is written only where the dock
# builds, paints or parks a card, which is what keeps it a ledger: demand
# travels as `update` payloads instead, the dock's `eager` set among them, so a
# block that core builds for another consumer (core's "Show code" asks for
# every block) never reads back as a card the dock built. Block removal needs
# no dock write -- core drops the slot, dropping the card from the ledger too.
built_cards <- function(visibility) {
ids <- ls(visibility$visible)
ids[lgl_ply(ids, slot_built, visibility$visible)]
Expand All @@ -156,41 +154,29 @@ slot_built <- function(id, visible) {
}

# Record `new` cards as built off screen: `visible` FALSE enters them in the
# ledger (built, not yet painted), `required` FALSE holds no construction
# demand until a view switch or the report observer places them. Slots
# pre-exist: core seeds every block's before any block plugin runs.
# ledger (built, not yet painted). Slots pre-exist: core seeds every block's
# before any block plugin runs.
mark_cards_built <- function(visibility, new) {
for (id in new) {
visibility$visible[[id]](FALSE)
visibility$required[[id]](FALSE)
}
}

# Reconcile the required axis over `built` from an on-screen set: on screen ->
# TRUE, off screen -> FALSE. A card leaving the screen keeps its ledger entry --
# `visible` goes FALSE (built, off screen), never NA (never built), or its view
# blanks on the next visit. The arrange observer owns marking a card painted
# (visible TRUE). Seeds the required axis too, with `built` the active view's
# membership.
show_cards <- function(visibility, built, on_screen) {
for (id in built) {
on <- id %in% on_screen

if (!identical(isolate(visibility$required[[id]]()), on)) {
visibility$required[[id]](on)
}

if (!on && isTRUE(isolate(visibility$visible[[id]]()))) {
# Park the cards that have left the screen: `visible` goes FALSE (built, off
# screen), never NA (never built), or their view blanks on the next visit.
mark_cards_hidden <- function(visibility, hidden) {
for (id in hidden) {
if (isTRUE(isolate(visibility$visible[[id]]()))) {
visibility$visible[[id]](FALSE)
}
}
}

# Mark a view's on-screen blocks painted: `visible` TRUE -- the client-confirmed
# paint core's render gate (is_visible = isTRUE) waits for. Unlike show_cards()
# this walks the caller's set and dereferences each slot, so that set must
# already be reconciled against core's visibility slots -- see
# report_visible_observer().
# paint core's render gate (is_visible = isTRUE) waits for. Unlike
# mark_cards_hidden(), which walks the ledger, this walks the caller's set and
# dereferences each slot, so that set must already be reconciled against core's
# visibility slots -- see report_visible_observer().
mark_cards_rendered <- function(visibility, on_screen) {
for (id in on_screen) {
if (!isTRUE(isolate(visibility$visible[[id]]()))) {
Expand Down
139 changes: 99 additions & 40 deletions R/board-server.R
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@
#'
#' @param board Reactive board state (list with `$board`).
#' @param update Reactive update signal from blockr.core.
#' @param visibility Per-block visibility channel from blockr.core: a list of
#' three reactiveVal environments (`required`, `visible`, `frozen`) the dock
#' writes to gate off-screen blocks and to freeze the inputs of blocks whose
#' controls are hidden.
#' @param visibility Visibility channel bundle from blockr.core: the per-block
#' reactiveVal environments `visible` (which cards it has painted) and
#' `frozen` (whose inputs it has hidden).
#' @param ... Extension server arguments.
#' @param plugins Served board plugins. Core threads these to its own block
#' server but not to callbacks, so `blockr_app_server.dock_board()` captures
Expand All @@ -19,7 +18,8 @@
#' @param session Shiny session.
#'
#' @return List with `dock`, `actions`, `view_data`, and extension
#' results.
#' results, plus the `eager()` declaration that makes the board lazy, which
#' core reads and strips before plugins see the list.
#'
#' @noRd
board_server_callback <- function(board, update, visibility, ...,
Expand Down Expand Up @@ -56,10 +56,10 @@ board_server_callback <- function(board, update, visibility, ...,
client_active <- reactiveVal(NULL)
client_views <- reactiveVal(seed_view_state(board_views(initial_board)))

# The `visibility` channel core hands us (per-block `required` / `visible` /
# `frozen` reactiveVal slots) is the single store: its `visible` axis is the
# dock's build ledger (!is.na = ever built), read via built_cards(). Stash it
# on active_dock -- the dock handle every card-touching path receives (view
# The `visibility` channel core hands us (per-block `visible` / `frozen`
# reactiveVal slots) is the single store: its `visible` axis is the dock's
# build ledger (!is.na = ever built), read via built_cards(). Stash it on
# active_dock -- the dock handle every card-touching path receives (view
# switch, panel-op apply, core insert / remove) -- so they read and write the
# one channel.
active_dock$visibility <- visibility
Expand Down Expand Up @@ -102,22 +102,24 @@ board_server_callback <- function(board, update, visibility, ...,
)
)

# Gate off-screen blocks from the first flush, before the client reports its
# layout (else core's all-visible default evaluates every block at startup).
# Seed to what board_ui rendered: the active view's whole membership is built
# (visible FALSE -- built, not yet painted), its front panels required TRUE
# and background tabs FALSE. Off-screen views' cards are built on first visit
# by switch_active_view. Core holds its render gate (is_visible = isTRUE)
# until the active view reports its blocks painted (visible TRUE).
# The dock makes the board lazy, and what board_ui rendered is its opening
# eager set: the active view's front panels. The declaration travels in the
# returned list rather than as a payload, since a payload applies at the tail
# of the flush it is written in, after that flush has decided what to
# construct. Core seeds the set as it runs the callbacks, so the layout echo
# only has to report a change.
owner <- dock_id(session$ns)
opening <- visible_block_ids(active_view_grid(initial_board))

hold_eager <- eager_holder(update, owner, opening)

# The active view's whole membership is built (visible FALSE -- built, not yet
# painted). Off-screen views' cards are built on first visit by
# switch_active_view. Core holds its render gate (is_visible = isTRUE) until
# the active view reports its blocks painted (visible TRUE).
mark_cards_built(visibility, active_view_block_ids(initial_board))

show_cards(
visibility,
active_view_block_ids(initial_board),
visible_block_ids(active_view_grid(initial_board))
)

report_visible_observer(visibility, client_active, docks)
report_visible_observer(visibility, hold_eager, client_active, docks)

# One row per group of the active view, restamped on a view switch since a
# different view stacks a different number of groups. The container is an
Expand Down Expand Up @@ -213,12 +215,14 @@ board_server_callback <- function(board, update, visibility, ...,

# Returned to core, spread into every plugin's args (see the two-bundle note
# above): `dock` for block placement, `view_data` for serialization, `actions`
# for the edit-block plugin, and the extensions' resolved results.
# for the edit-block plugin, and the extensions' resolved results. The `eager`
# declaration is core's to read, and never reaches a plugin.
list(
dock = active_dock,
actions = triggers,
view_data = view_data,
extensions = ext_res
extensions = ext_res,
eager = eager(owner, opening)
)
}

Expand Down Expand Up @@ -284,19 +288,21 @@ switch_view_observer <- function(session, update, client_active, board, docks,
)
}

report_visible_observer <- function(visibility, client_active, docks) {

# Drives both visibility axes off two live client signals: the active view's
# settled `_state` layout echo (`dock$layout()`, the arrangement dockView
# painted) and its live active panel (`dock$active_panel()`). Over the built
# cards, the front panels go required TRUE and are marked painted on the
# visible axis (the client-confirmed paint core's render gate waits for);
# everything else built goes required FALSE with its visible slot cleared.
# A bare tab switch does not reliably re-echo `_state` (only structural
# gestures do), so the active panel is folded in as the front of its group --
# otherwise a newly-fronted tab is never marked visible and its block stays
# blank until a structural change. `req(layout())` waits for the client's
# first report (NULL before then); `active_panel()` is NULL until a switch.
report_visible_observer <- function(visibility, hold_eager, client_active,
docks) {

# Drives the dock's demand and its paint report off two live client signals:
# the active view's settled `_state` layout echo (`dock$layout()`, the
# arrangement dockView painted) and its live active panel
# (`dock$active_panel()`). The front panels are held eager and marked painted
# on the visible axis (the client-confirmed paint core's render gate waits
# for); everything else built is released by its absence from the eager set
# and parked in the ledger. A bare tab switch does not reliably re-echo
# `_state` (only structural gestures do), so the active panel is folded in as
# the front of its group -- otherwise a newly-fronted tab is never marked
# visible and its block stays blank until a structural change.
# The `req(layout())` guard waits for the client's first report (NULL before
# then); `active_panel()` is NULL until a switch.
#
# The echo is the client's account of what is on screen and core's visibility
# slots are the server's account of which blocks exist, so the two disagree
Expand Down Expand Up @@ -324,12 +330,62 @@ report_visible_observer <- function(visibility, client_active, docks) {
{
req(client_active())

show_cards(visibility, built_cards(visibility), on_screen())
hold_eager(on_screen())

mark_cards_hidden(
visibility,
setdiff(built_cards(visibility), on_screen())
)

mark_cards_rendered(visibility, on_screen())
}
)
}

# The dock's evaluation demand: the blocks it has on screen, held eager under
# the owner label it declared. The `set` verb carries the whole eager set, so a
# card that left the screen is released by its absence -- one payload per
# switch where the retired `required` channel took a write per slot. Sent only
# when the set changes from what core holds, which starts as the declared
# opening set, since a layout echo re-reports the same set and every payload is
# a board-update round trip.
eager_holder <- function(update, owner, opening) {

sent <- new.env(parent = emptyenv())
sent$ids <- sort(opening)

function(on_screen) {

ids <- sort(on_screen)

if (identical(sent$ids, ids)) {
return(invisible())
}

sent$ids <- ids

fold_update(update, list(eager = set_names(list(list(set = ids)), owner)))

invisible()
}
}

# Core drains the update channel once per flush, so a second writer before that
# apply replaces the first payload whole rather than adding to it. The settled
# `_state` echo drives two of them -- the geometry mirror and the eager holder
# above -- and either can run first, so both fold into what is already pending
# instead of overwriting it. Folding an `eager` component into a state-carrying
# payload cannot cost it its lock exemption: the mirror is only wired on an
# unlocked board.
fold_update <- function(update, payload) {
update(
utils::modifyList(
coal(isolate(update()), list(), fail_all = FALSE),
payload
)
)
}

# Drive blockr.core's per-block `frozen` channel off the block-card section
# toggles. A block whose "inputs" section is hidden -- and every block on a
# locked board, whose controls are read-only -- is frozen: core pins its
Expand Down Expand Up @@ -893,7 +949,10 @@ manage_dock <- function(
if (!is_dock_locked() && !narrow) {

commit_grid <- function(grid) {
update(list(views = list(grid = set_names(list(grid), id))))
fold_update(
update,
list(views = list(grid = set_names(list(grid), id)))
)
}

observe_grid_echo(id, dock, board, commit_grid)
Expand Down
6 changes: 5 additions & 1 deletion R/utils-serve.R
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ grids_stable <- function(stored, live) {
# * `roundtrip_stable` -- `TRUE` once every view's stored placement matches
# what the client echoes, tree and rails alike (`NA` until every view has
# reported a layout), so a restore push provokes no spurious commit.
# * `stored_grids` -- the board's stored placement, for asserting that no
# write-back happened. The update tally cannot say that on its own: the
# dock's `eager` payloads ride the same channel as its commits.
#' @exportS3Method blockr.core::blockr_test_exports
blockr_test_exports.dock_board <- function(x, rv, ...) {

Expand All @@ -151,6 +154,7 @@ blockr_test_exports.dock_board <- function(x, rv, ...) {
} else {
grids_stable(board_grids(rv[["board"]]$board), vd[["grids"]])
}
}
},
stored_grids = board_grids(rv[["board"]]$board)
)
}
28 changes: 20 additions & 8 deletions tests/testthat/helpers.R
Original file line number Diff line number Diff line change
Expand Up @@ -159,28 +159,40 @@ fire_action <- function(gen, trigger, board) {
}

# Stand-in for the `visibility` channel blockr.core hands the board callback:
# three environments of per-block reactiveVals (`required`, `visible`,
# `frozen`), one slot per block, mirroring core's add_vis_slots at construction
# (which seeds every board block before the callback runs). `visible` is logical
# (the dock's build ledger: !is.na = ever built), matching core's slot. The dock
# writes values into these slots; core owns their lifecycle in the real thing.
# Pass the block ids to seed, or a board handle to seed from its blocks.
# two environments of per-block reactiveVals (`visible`, `frozen`), one slot
# per block, mirroring core's add_vis_slots at construction (which seeds every
# board block before the callback runs). The `visible` slot is logical (the
# dock's build ledger: !is.na = ever built), matching core's. The dock writes
# values into these slots; core owns their lifecycle in the real thing. Pass
# the block ids to seed, or a board handle to seed from its blocks.
fake_visibility <- function(x = character()) {
ids <- if (is.character(x)) x else board_block_ids(shiny::isolate(x$board))

vis <- list(
required = new.env(parent = emptyenv()),
visible = new.env(parent = emptyenv()),
frozen = new.env(parent = emptyenv())
)
for (id in ids) {
vis$required[[id]] <- shiny::reactiveVal(NA)
vis$visible[[id]] <- shiny::reactiveVal(NA)
vis$frozen[[id]] <- shiny::reactiveVal(FALSE)
}
vis
}

# The blocks the dock holds eager in `update`'s pending payload, as core would
# read them: one `eager` delta, keyed by the owner label the dock declared. The
# delta is a `set`, so this is the whole of what the dock holds.
held_eager <- function(update, owner = NULL) {
held <- shiny::isolate(update())[["eager"]]

if (is.null(owner)) {
testthat::expect_length(held, 1L)
owner <- names(held)
}

held[[owner]][["set"]]
}

# Resolve a view's stable id from its display label. Views are keyed by
# id internally; tests that know a view by its label use this to reach
# the id (labels are unique within the fixtures).
Expand Down
Loading
Loading