Merge branch 'master' into bookmarkable-state

This commit is contained in:
Winston Chang
2016-07-29 15:47:31 -05:00
29 changed files with 562 additions and 351 deletions

View File

@@ -1,7 +1,7 @@
Package: shiny
Type: Package
Title: Web Application Framework for R
Version: 0.13.2.9003
Version: 0.13.2.9004
Date: 2016-02-17
Authors@R: c(
person("Winston", "Chang", role = c("aut", "cre"), email = "winston@rstudio.com"),

View File

@@ -11,6 +11,7 @@ S3method("[",shinyoutput)
S3method("[<-",reactivevalues)
S3method("[<-",shinyoutput)
S3method("[[",reactivevalues)
S3method("[[",session_proxy)
S3method("[[",shinyoutput)
S3method("[[<-",reactivevalues)
S3method("[[<-",shinyoutput)

14
NEWS
View File

@@ -1,5 +1,11 @@
shiny 0.13.2.9001
--------------------------------------------------------------------------------
* Added support for the `pool` package (use Shiny's timer/scheduler)
* `Display: Showcase` now displays the .js, .html and .css files in the `www`
directory by default. In order to use showcase mode and not display these,
include a new line in your Description file: `IncludeWWW: False`.
* Added insertUI and removeUI functions to be able to add and remove chunks
of UI, standalone, and all independent of one another.
@@ -28,6 +34,8 @@ shiny 0.13.2.9001
`showNotification` and `hideNotification`. From JavaScript, there is a new
`Shiny.notification` object that controls notifications. (#1141)
* Progress indicators now use the notification API.
* Improved `renderTable()` function to make the tables look prettier and also
provide the user with a lot more parameters to customize their tables with.
@@ -71,6 +79,12 @@ shiny 0.13.2.9001
* Almost all code examples now have a runnable example with `shinyApp()`, so
that users can run the examples and see them in action. (#1137, #1158)
* Added `session$resetBrush(brushId)` (R) and `Shiny.resetBrush(brushId)` (JS)
to programatically clear brushes from `imageOutput`/`plotOutput`.
* Fixed #1253: Memory could leak when an observer was destroyed without first
being invalidated.
shiny 0.13.2
--------------------------------------------------------------------------------

View File

@@ -12,22 +12,11 @@
#' appropriate \code{render} function or a customized \code{reactive}
#' function. To remove any part of your UI, use \code{\link{removeUI}}.
#'
#' Note that whatever UI object you pass through \code{ui}, it is always
#' wrapped in an extra \code{div} (or if \code{inline = TRUE}, a
#' \code{span}) before making its way into the DOM. This does not affect
#' what you mean to do, and it makes it easier to remove the whole UI
#' object using \code{\link{removeUI}} (if you wish to do so, of course).
#'
#' @param selector A string that is accepted by jQuery's selector (i.e. the
#' string \code{s} to be placed in a \code{$(s)} jQuery call). This selector
#' will determine the element(s) relative to which you want to insert your
#' UI object.
#'
#' @param multiple In case your selector matches more than one element,
#' \code{multiple} determines whether Shiny should insert the UI object
#' relative to all matched elements or just relative to the first
#' matched element (default).
#'
#' @param where Where your UI object should go relative to the selector:
#' \describe{
#' \item{\code{beforeBegin}}{Before the selector element itself}
@@ -41,18 +30,22 @@
#' \href{https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML}{here}.
#'
#' @param ui The UI object you want to insert. This can be anything that
#' you usually put inside your apps's \code{ui} function.
#' you usually put inside your apps's \code{ui} function. If you're inserting
#' multiple elements in one call, make sure to wrap them in either a
#' \code{tagList()} or a \code{tags$div()} (the latter option has the
#' advantage that you can give it an \code{id} to make it easier to
#' reference or remove it later on). If you want to insert raw html, use
#' \code{ui = HTML()}.
#'
#' @param multiple In case your selector matches more than one element,
#' \code{multiple} determines whether Shiny should insert the UI object
#' relative to all matched elements or just relative to the first
#' matched element (default).
#'
#' @param immediate Whether the UI object should be immediately inserted into
#' the app when you call \code{insertUI}, or whether Shiny should wait until
#' all outputs have been updated and all observers have been run (default).
#'
#' @param container A function to generate an HTML element to contain the UI
#' object.
#'
#' @param inline Use an inline (\code{span()}) or block container (\code{div()},
#' default) for the output.
#'
#' @param session The shiny session within which to call \code{insertUI}.
#'
#' @seealso \code{\link{removeUI}}
@@ -83,19 +76,16 @@
#'
#' @export
insertUI <- function(selector,
multiple = FALSE,
where = c("beforeBegin", "afterBegin", "beforeEnd", "afterEnd"),
ui,
multiple = FALSE,
immediate = FALSE,
container = if (inline) "span" else "div",
inline = FALSE,
session = getDefaultReactiveDomain()) {
force(selector)
force(ui)
force(session)
force(multiple)
force(container)
if (missing(where)) where <- "beforeEnd"
where <- match.arg(where)
@@ -103,8 +93,7 @@ insertUI <- function(selector,
session$sendInsertUI(selector = selector,
multiple = multiple,
where = where,
content = processDeps(ui, session),
container = container)
content = processDeps(ui, session))
}
if (!immediate) session$onFlushed(callback, once = TRUE)
@@ -129,7 +118,9 @@ insertUI <- function(selector,
#' string \code{s} to be placed in a \code{$(s)} jQuery call). This selector
#' will determine the element(s) to be removed. If you want to remove a
#' Shiny input or output, note that many of these are wrapped in \code{div}s,
#' so you may need to use a somewhat complex selector (see the Examples below).
#' so you may need to use a somewhat complex selector -- see the Examples below.
#' (Alternatively, you could also wrap the inputs/outputs that you want to be
#' able to remove easily in a \code{div} with an id.)
#'
#' @param multiple In case your selector matches more than one element,
#' \code{multiple} determines whether Shiny should remove all the matched

View File

@@ -1,4 +1,4 @@
# Creates an object whose $ and $<- pass through to the parent
# Creates an object whose $ and [[ pass through to the parent
# session, unless the name is matched in ..., in which case
# that value is returned instead. (See Decorator pattern.)
createSessionProxy <- function(parentSession, ...) {
@@ -14,18 +14,24 @@ createSessionProxy <- function(parentSession, ...) {
#' @export
`$.session_proxy` <- function(x, name) {
if (name %in% names(x[["overrides"]]))
x[["overrides"]][[name]]
if (name %in% names(.subset2(x, "overrides")))
.subset2(x, "overrides")[[name]]
else
x[["parent"]][[name]]
.subset2(x, "parent")[[name]]
}
#' @export
`[[.session_proxy` <- `$.session_proxy`
#' @export
`$<-.session_proxy` <- function(x, name, value) {
x[["parent"]][[name]] <- value
x
stop("Attempted to assign value on session proxy.")
}
`[[<-.session_proxy` <- `$<-.session_proxy`
#' Invoke a Shiny module
#'
#' Shiny's module feature lets you break complicated UI and server logic into

View File

@@ -42,11 +42,11 @@ NULL
#
## ------------------------------------------------------------------------
createMockDomain <- function() {
callbacks <- list()
callbacks <- Callbacks$new()
ended <- FALSE
domain <- new.env(parent = emptyenv())
domain$onEnded <- function(callback) {
callbacks <<- c(callbacks, callback)
return(callbacks$register(callback))
}
domain$isEnded <- function() {
ended
@@ -55,7 +55,7 @@ createMockDomain <- function() {
domain$end <- function() {
if (!ended) {
ended <<- TRUE
lapply(callbacks, do.call, list())
callbacks$invoke()
}
invisible()
}

View File

@@ -714,12 +714,18 @@ Observer <- R6Class(
.domain = 'ANY',
.priority = numeric(0),
.autoDestroy = logical(0),
# A function that, when invoked, unsubscribes the autoDestroy
# listener (or NULL if autodestroy is disabled for this observer).
# We must unsubscribe when this observer is destroyed, or else
# the observer cannot be garbage collected until the session ends.
.autoDestroyHandle = 'ANY',
.invalidateCallbacks = list(),
.execCount = integer(0),
.onResume = 'function',
.suspended = logical(0),
.destroyed = logical(0),
.prevId = character(0),
.ctx = NULL,
initialize = function(observerFunc, label, suspended = FALSE, priority = 0,
domain = getDefaultReactiveDomain(),
@@ -742,7 +748,6 @@ registerDebugHook("observerFunc", environment(), label)
}
.label <<- label
.domain <<- domain
.autoDestroy <<- autoDestroy
.priority <<- normalizePriority(priority)
.execCount <<- 0L
.suspended <<- suspended
@@ -750,7 +755,9 @@ registerDebugHook("observerFunc", environment(), label)
.destroyed <<- FALSE
.prevId <<- ''
onReactiveDomainEnded(.domain, self$.onDomainEnded)
.autoDestroy <<- FALSE
.autoDestroyHandle <<- NULL
setAutoDestroy(autoDestroy)
# Defer the first running of this until flushReact is called
.createContext()$invalidate()
@@ -759,7 +766,23 @@ registerDebugHook("observerFunc", environment(), label)
ctx <- Context$new(.domain, .label, type='observer', prevId=.prevId)
.prevId <<- ctx$id
if (!is.null(.ctx)) {
# If this happens, something went wrong.
warning("Created a new context without invalidating previous context.")
}
# Store the context explicitly in the Observer object. This is necessary
# to make sure that when the observer is destroyed, it also gets
# invalidated. Otherwise the upstream reactive (on which the observer
# depends) will hold a (indirect) reference to this context until the
# reactive is invalidated, which may not happen immediately or at all.
# This can lead to a memory leak (#1253).
.ctx <<- ctx
ctx$onInvalidate(function() {
# Context is invalidated, so we don't need to store a reference to it
# anymore.
.ctx <<- NULL
lapply(.invalidateCallbacks, function(invalidateCallback) {
invalidateCallback()
NULL
@@ -812,11 +835,28 @@ registerDebugHook("observerFunc", environment(), label)
"Sets whether this observer should be automatically destroyed when its
domain (if any) ends. If autoDestroy is TRUE and the domain already
ended, then destroy() is called immediately."
if (.autoDestroy == autoDestroy) {
return(.autoDestroy)
}
oldValue <- .autoDestroy
.autoDestroy <<- autoDestroy
if (!is.null(.domain) && .domain$isEnded()) {
destroy()
if (autoDestroy) {
if (!.destroyed && !is.null(.domain)) { # Make sure to not try to destroy twice.
if (.domain$isEnded()) {
destroy()
} else {
.autoDestroyHandle <<- onReactiveDomainEnded(.domain, .onDomainEnded)
}
}
} else {
if (!is.null(.autoDestroyHandle))
.autoDestroyHandle()
.autoDestroyHandle <<- NULL
}
invisible(oldValue)
},
suspend = function() {
@@ -842,8 +882,21 @@ registerDebugHook("observerFunc", environment(), label)
"Prevents this observer from ever executing again (even if a flush has
already been scheduled)."
# Make sure to not try to destory twice.
if (.destroyed)
return()
suspend()
.destroyed <<- TRUE
if (!is.null(.autoDestroyHandle)) {
.autoDestroyHandle()
}
.autoDestroyHandle <<- NULL
if (!is.null(.ctx)) {
.ctx$invalidate()
}
},
.onDomainEnded = function() {
if (isTRUE(.autoDestroy)) {

View File

@@ -254,7 +254,7 @@ createAppHandlers <- function(httpHandlers, serverFuncSource) {
if (length(splitName) > 1) {
if (!inputHandlers$containsKey(splitName[[2]])) {
# No input handler registered for this type
stop("No handler registered for for type ", name)
stop("No handler registered for type ", name)
}
inputName <- splitName[[1]]
@@ -592,7 +592,8 @@ runApp <- function(appDir=getwd(),
host <- '0.0.0.0'
# Make warnings print immediately
ops <- options(warn = 1)
# Set pool.scheduler to support pool package
ops <- options(warn = 1, pool.scheduler = scheduleTask)
on.exit(options(ops), add = TRUE)
workerId(workerId)
@@ -629,21 +630,38 @@ runApp <- function(appDir=getwd(),
on.exit(close(con), add = TRUE)
settings <- read.dcf(con)
if ("DisplayMode" %in% colnames(settings)) {
mode <- settings[1,"DisplayMode"]
mode <- settings[1, "DisplayMode"]
if (mode == "Showcase") {
setShowcaseDefault(1)
if ("IncludeWWW" %in% colnames(settings)) {
.globals$IncludeWWW <- as.logical(settings[1, "IncludeWWW"])
if (is.na(.globals$IncludeWWW)) {
stop("In your Description file, `IncludeWWW` ",
"must be set to `True` (default) or `False`")
}
} else {
.globals$IncludeWWW <- TRUE
}
}
}
}
}
## default is to show the .js, .css and .html files in the www directory
## (if not in showcase mode, this variable will simply be ignored)
if (is.null(.globals$IncludeWWW) || is.na(.globals$IncludeWWW)) {
.globals$IncludeWWW <- TRUE
}
# If display mode is specified as an argument, apply it (overriding the
# value specified in DESCRIPTION, if any).
display.mode <- match.arg(display.mode)
if (display.mode == "normal")
if (display.mode == "normal") {
setShowcaseDefault(0)
else if (display.mode == "showcase")
}
else if (display.mode == "showcase") {
setShowcaseDefault(1)
}
require(shiny)
@@ -664,7 +682,14 @@ runApp <- function(appDir=getwd(),
}
else {
# Try up to 20 random ports
port <- p_randomInt(3000, 8000)
while (TRUE) {
port <- p_randomInt(3000, 8000)
# Reject ports in this range that are considered unsafe by Chrome
# http://superuser.com/questions/188058/which-ports-are-considered-unsafe-on-chrome
if (!port %in% c(3659, 4045, 6000, 6665:6669)) {
break
}
}
}
# Test port to see if we can use it

View File

@@ -172,6 +172,18 @@ workerId <- local({
#' example, \code{session$clientData$url_search}).
#'
#' @return
#' \item{allowReconnect(value)}{
#' If \code{value} is \code{TRUE} and run in a hosting environment (Shiny
#' Server or Connect) with reconnections enabled, then when the session ends
#' due to the network connection closing, the client will attempt to
#' reconnect to the server. If a reconnection is successful, the browser will
#' send all the current input values to the new session on the server, and
#' the server will recalculate any outputs and send them back to the client.
#' If \code{value} is \code{FALSE}, reconnections will be disabled (this is
#' the default state). If \code{"force"}, then the client browser will always
#' attempt to reconnect. The only reason to use \code{"force"} is for testing
#' on a local connection (without Shiny Server or Connect).
#' }
#' \item{clientData}{
#' A \code{\link{reactiveValues}} object that contains information about the client.
#' \itemize{
@@ -206,6 +218,11 @@ workerId <- local({
#' \item{isClosed()}{A function that returns \code{TRUE} if the client has
#' disconnected.
#' }
#' \item{ns(id)}{
#' Server-side version of \code{ns <- \link{NS}(id)}. If bare IDs need to be
#' explicitly namespaced for the current module, \code{session$ns("name")}
#' will return the fully-qualified ID.
#' }
#' \item{onEnded(callback)}{
#' Synonym for \code{onSessionEnded}.
#' }
@@ -253,17 +270,9 @@ workerId <- local({
#' This is the request that was used to initiate the websocket connection
#' (as opposed to the request that downloaded the web page for the app).
#' }
#' \item{allowReconnect(value)}{
#' If \code{value} is \code{TRUE} and run in a hosting environment (Shiny
#' Server or Connect) with reconnections enabled, then when the session ends
#' due to the network connection closing, the client will attempt to
#' reconnect to the server. If a reconnection is successful, the browser will
#' send all the current input values to the new session on the server, and
#' the server will recalculate any outputs and send them back to the client.
#' If \code{value} is \code{FALSE}, reconnections will be disabled (this is
#' the default state). If \code{"force"}, then the client browser will always
#' attempt to reconnect. The only reason to use \code{"force"} is for testing
#' on a local connection (without Shiny Server or Connect).
#' \item{resetBrush(brushId)}{
#' Resets/clears the brush with the given \code{brushId}, if it exists on
#' any \code{imageOutput} or \code{plotOutput} in the app.
#' }
#' \item{sendCustomMessage(type, message)}{
#' Sends a custom message to the web page. \code{type} must be a
@@ -285,11 +294,6 @@ workerId <- local({
#' from Shiny apps, but through friendlier wrapper functions like
#' \code{\link{updateTextInput}}.
#' }
#' \item{ns(id)}{
#' Server-side version of \code{ns <- \link{NS}(id)}. If bare IDs need to be
#' explicitly namespaced for the current module, \code{session$ns("name")}
#' will return the fully-qualified ID.
#' }
#'
#' @name session
NULL
@@ -1122,15 +1126,13 @@ ShinySession <- R6Class(
reload = function() {
private$sendMessage(reload = TRUE)
},
sendInsertUI = function(selector, multiple, where,
content, container) {
sendInsertUI = function(selector, multiple, where, content) {
private$sendMessage(
`shiny-insert-ui` = list(
selector = selector,
multiple = multiple,
where = where,
content = content,
container = container
content = content
)
)
},
@@ -1145,6 +1147,13 @@ ShinySession <- R6Class(
updateLocationBar = function(url) {
private$sendMessage(updateLocationBar = list(url = url))
},
resetBrush = function(brushId) {
private$sendMessage(
resetBrush = list(
brushId = brushId
)
)
},
# Public RPC methods
`@uploadieFinish` = function() {
@@ -1524,10 +1533,15 @@ ShinySession <- R6Class(
#' @param ... Options to set for the output observer.
#' @export
outputOptions <- function(x, name, ...) {
if (!inherits(x, "shinyoutput"))
if (!inherits(x, "shinyoutput")) {
stop("x must be a shinyoutput object.")
}
name <- .subset2(x, 'ns')(name)
if (!missing(name)) {
name <- .subset2(x, 'ns')(name)
} else {
name <- NULL
}
.subset2(x, 'impl')$outputOptions(name, ...)
}

View File

@@ -77,10 +77,60 @@ appMetadata <- function(desc) {
else ""
}
navTabsHelper <- function(files, prefix = "") {
lapply(files, function(file) {
with(tags,
li(class=if (tolower(file) %in% c("app.r", "server.r")) "active" else "",
a(href=paste("#", gsub(".", "_", file, fixed=TRUE), "_code", sep=""),
"data-toggle"="tab", paste0(prefix, file)))
)
})
}
navTabsDropdown <- function(files) {
if (length(files) > 0) {
with(tags,
li(role="presentation", class="dropdown",
a(class="dropdown-toggle", `data-toggle`="dropdown", href="#",
role="button", `aria-haspopup`="true", `aria-expanded`="false",
"www", span(class="caret")
),
ul(class="dropdown-menu", navTabsHelper(files))
)
)
}
}
tabContentHelper <- function(files, path, language) {
lapply(files, function(file) {
with(tags,
div(class=paste("tab-pane",
if (tolower(file) %in% c("app.r", "server.r")) " active"
else "",
sep=""),
id=paste(gsub(".", "_", file, fixed=TRUE),
"_code", sep=""),
pre(class="shiny-code",
# we need to prevent the indentation of <code> ... </code>
HTML(format(tags$code(
class=paste0("language-", language),
paste(readUTF8(file.path.ci(path, file)), collapse="\n")
), indent = FALSE))))
)
})
}
# Returns tags containing the application's code in Bootstrap-style tabs in
# showcase mode.
showcaseCodeTabs <- function(codeLicense) {
rFiles <- list.files(pattern = "\\.[rR]$")
wwwFiles <- list()
if (isTRUE(.globals$IncludeWWW)) {
path <- file.path(getwd(), "www")
wwwFiles$jsFiles <- list.files(path, pattern = "\\.js$")
wwwFiles$cssFiles <- list.files(path, pattern = "\\.css$")
wwwFiles$htmlFiles <- list.files(path, pattern = "\\.html$")
}
with(tags, div(id="showcase-code-tabs",
a(id="showcase-code-position-toggle",
class="btn btn-default btn-sm",
@@ -88,27 +138,21 @@ showcaseCodeTabs <- function(codeLicense) {
icon("level-up"),
"show with app"),
ul(class="nav nav-tabs",
lapply(rFiles, function(rFile) {
li(class=if (tolower(rFile) %in% c("app.r", "server.r")) "active" else "",
a(href=paste("#", gsub(".", "_", rFile, fixed=TRUE),
"_code", sep=""),
"data-toggle"="tab", rFile))
})),
navTabsHelper(rFiles),
navTabsDropdown(unlist(wwwFiles))
),
div(class="tab-content", id="showcase-code-content",
lapply(rFiles, function(rFile) {
div(class=paste("tab-pane",
if (tolower(rFile) %in% c("app.r", "server.r")) " active"
else "",
sep=""),
id=paste(gsub(".", "_", rFile, fixed=TRUE),
"_code", sep=""),
pre(class="shiny-code",
# we need to prevent the indentation of <code> ... </code>
HTML(format(tags$code(
class="language-r",
paste(readUTF8(file.path.ci(getwd(), rFile)), collapse="\n")
), indent = FALSE))))
})),
tabContentHelper(rFiles, path = getwd(), language = "r"),
tabContentHelper(wwwFiles$jsFiles,
path = paste0(getwd(), "/www"),
language = "javascript"),
tabContentHelper(wwwFiles$cssFiles,
path = paste0(getwd(), "/www"),
language = "css"),
tabContentHelper(wwwFiles$htmlFiles,
path = paste0(getwd(), "/www"),
language = "xml")
),
codeLicense))
}
@@ -177,3 +221,4 @@ showcaseUI <- function(ui) {
showcaseBody(ui)
)
}

View File

@@ -71,3 +71,16 @@ TimerCallbacks <- R6Class(
)
timerCallbacks <- TimerCallbacks$new()
scheduleTask <- function(millis, callback) {
cancelled <- FALSE
timerCallbacks$schedule(millis, function() {
if (!cancelled)
callback()
})
function() {
cancelled <<- TRUE
callback <<- NULL # to allow for callback to be gc'ed
}
}

View File

@@ -634,7 +634,10 @@ Callbacks <- R6Class(
.callbacks = 'Map',
initialize = function() {
.nextId <<- as.integer(.Machine$integer.max)
# NOTE: we avoid using '.Machine$integer.max' directly
# as R 3.3.0's 'radixsort' could segfault when sorting
# an integer vector containing this value
.nextId <<- as.integer(.Machine$integer.max - 1L)
.callbacks <<- Map$new()
},
register = function(callback) {

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +1,7 @@
code {
line-height: 150%;
}
pre .operator,
pre .paren {
color: rgb(104, 118, 135)
@@ -13,9 +17,11 @@ pre .number {
pre .comment {
color: rgb(76, 136, 107);
font-style: italic;
}
pre .keyword {
pre .keyword,
pre .id {
color: rgb(0, 0, 255);
}
@@ -23,7 +29,53 @@ pre .identifier {
color: rgb(0, 0, 0);
}
pre .string {
pre .string,
pre .attribute {
color: rgb(3, 106, 7);
}
pre .doctype {
color: rgb(104, 104, 92);
}
pre .tag,
pre .title {
color: rgb(4, 29, 140);
}
pre .value {
color: rgb(13, 105, 18);
}
.language-xml .attribute {
color: rgb(0, 0, 0);
}
.language-css .attribute {
color: rgb(110, 124, 219);
}
.language-css .value {
color: rgb(23, 149, 30);
}
.language-css .number,
.language-css .hexcolor {
color: rgb(7, 27, 201);
}
.language-css .function {
color: rgb(61, 77, 113);
}
.language-css .tag {
color: rgb(195, 13, 25);
}
.language-css .class {
color: rgb(53, 132, 148);
}
.language-css .pseudo {
color: rgb(13, 105, 18);
}

View File

@@ -54,13 +54,13 @@
}
}
}
// If this is not a text node, descend recursively to see how many
// If this is not a text node, descend recursively to see how many
// lines it contains.
else if (child.nodeType === 1) { // ELEMENT_NODE
var ret = findTextPoint(child, line - newlines, col);
if (ret.element !== null)
return ret;
else
return ret;
else
newlines += ret.offset;
}
}
@@ -85,7 +85,7 @@
var code = document.getElementById(srcfile.replace(/\./g, "_") + "_code");
var start = findTextPoint(code, ref[0], ref[4]);
var end = findTextPoint(code, ref[2], ref[5]);
// If the insertion point can't be found, bail out now
if (start.element === null || end.element === null)
return;
@@ -129,10 +129,10 @@
var animateCodeMs = animate ? animateMs : 1;
// set the source and targets for the tab move
var newHostElement = above ?
var newHostElement = above ?
document.getElementById("showcase-sxs-code") :
document.getElementById("showcase-code-inline");
var currentHostElement = above ?
var currentHostElement = above ?
document.getElementById("showcase-code-inline") :
document.getElementById("showcase-sxs-code");
@@ -162,7 +162,7 @@
$(newHostElement).fadeIn();
if (!above) {
// remove the applied width and zoom on the app container, and
// remove the applied width and zoom on the app container, and
// scroll smoothly down to the code's new home
document.getElementById("showcase-app-container").removeAttribute("style");
if (animate)
@@ -234,9 +234,9 @@
};
// make the code scrollable to about the height of the browser, less space
// for the tabs
// for the tabs
var setCodeHeightFromDocHeight = function() {
document.getElementById("showcase-code-content").style.height =
document.getElementById("showcase-code-content").style.height =
$(window).height() + "px";
};
@@ -247,7 +247,7 @@
// IE8 puts the content of <script> tags into innerHTML but
// not innerText
var content = mdContent.innerText || mdContent.innerHTML;
document.getElementById("readme-md").innerHTML =
document.getElementById("readme-md").innerHTML =
(new Showdown.converter()).makeHtml(content)
}
}

View File

@@ -124,46 +124,17 @@
max-width: 100%;
}
.shiny-progress-container {
position: fixed;
top: 0px;
width: 100%;
/* Make sure it draws above all Bootstrap components */
z-index: 2000;
}
.shiny-progress .progress {
position: absolute;
width: 100%;
top: 0px;
height: 3px;
margin: 0px;
}
.shiny-progress .bar {
opacity: 0.6;
transition-duration: 250ms;
}
.shiny-progress .progress-text {
position: absolute;
right: 10px;
height: 24px;
width: 240px;
background-color: #eef8ff;
margin: 0px;
padding: 2px 3px;
opacity: 0.85;
margin-bottom: 5px;
height: 10px;
}
.shiny-progress .progress-text .progress-message {
padding: 0px 3px;
font-weight: bold;
font-size: 90%;
}
.shiny-progress .progress-text .progress-detail {
padding: 0px 3px;
font-size: 80%;
}

View File

@@ -200,30 +200,6 @@ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol
return val.replace(/([!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~])/g, '\\$1');
};
// Helper function for addMessageHandler('shiny-insert-ui').
// Turns out that Firefox does not support insertAdjacentElement().
// So we have to implement our own version for insertUI.
function insertAdjacentElement(where, element, content) {
switch (where) {
case 'beforeBegin':
element.parentNode.insertBefore(content, element);
break;
case 'afterBegin':
element.insertBefore(content, element.firstChild);
break;
case 'beforeEnd':
element.appendChild(content);
break;
case 'afterEnd':
if (element.nextSibling) {
element.parentNode.insertBefore(content, element.nextSibling);
} else {
element.parentNode.appendChild(content);
}
break;
}
}
//---------------------------------------------------------------------
// Source file: ../srcjs/browser.js
@@ -1168,9 +1144,7 @@ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol
exports.renderHtml($([]), message.content.html, message.content.deps);
} else {
targets.each(function (i, target) {
var container = document.createElement(message.container);
insertAdjacentElement(message.where, target, container);
exports.renderContent(container, message.content);
exports.renderContent(target, message.content, message.where);
return message.multiple;
});
}
@@ -1192,6 +1166,10 @@ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol
window.history.replaceState(null, null, message.url);
});
addMessageHandler("resetBrush", function (message) {
exports.resetBrush(message.brushId);
});
// Progress reporting ====================================================
var progressHandlers = {
@@ -1203,35 +1181,24 @@ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol
binding.showProgress(true);
}
},
// Open a page-level progress bar
open: function open(message) {
// Add progress container (for all progress items) if not already present
var $container = $('.shiny-progress-container');
if ($container.length === 0) {
$container = $('<div class="shiny-progress-container"></div>');
$('body').append($container);
}
// Add div for just this progress ID
var depth = $('.shiny-progress.open').length;
var $progress = $(progressHandlers.progressHTML);
$progress.attr('id', message.id);
$container.append($progress);
// Stack bars
var $progressBar = $progress.find('.progress');
$progressBar.css('top', depth * $progressBar.height() + 'px');
// Stack text objects
var $progressText = $progress.find('.progress-text');
$progressText.css('top', 3 * $progressBar.height() + depth * $progressText.outerHeight() + 'px');
$progress.hide();
// Progress bar starts hidden; will be made visible if a value is provided
// during updates.
exports.notifications.show({
html: '<div id="shiny-progress-' + message.id + '" class="shiny-progress">' + '<div class="progress progress-striped active" style="display: none;"><div class="progress-bar"></div></div>' + '<div class="progress-text">' + '<span class="progress-message">message</span> ' + '<span class="progress-detail"></span>' + '</div>' + '</div>',
id: message.id,
duration: null
});
},
// Update page-level progress bar
update: function update(message) {
var $progress = $('#' + message.id + '.shiny-progress');
var $progress = $('#shiny-progress-' + message.id);
if ($progress.length === 0) return;
if (typeof message.message !== 'undefined') {
$progress.find('.progress-message').text(message.message);
}
@@ -1241,32 +1208,17 @@ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol
if (typeof message.value !== 'undefined') {
if (message.value !== null) {
$progress.find('.progress').show();
$progress.find('.bar').width(message.value * 100 + '%');
$progress.find('.progress-bar').width(message.value * 100 + '%');
} else {
$progress.find('.progress').hide();
}
}
$progress.fadeIn();
},
// Close page-level progress bar
close: function close(message) {
var $progress = $('#' + message.id + '.shiny-progress');
$progress.removeClass('open');
$progress.fadeOut({
complete: function complete() {
$progress.remove();
// If this was the last shiny-progress, remove container
if ($('.shiny-progress').length === 0) $('.shiny-progress-container').remove();
}
});
},
// The 'bar' class is needed for backward compatibility with Bootstrap 2.
progressHTML: '<div class="shiny-progress open">' + '<div class="progress progress-striped active"><div class="progress-bar bar"></div></div>' + '<div class="progress-text">' + '<span class="progress-message">message</span>' + '<span class="progress-detail"></span>' + '</div>' + '</div>'
exports.notifications.remove(message.id);
}
};
exports.progressHandlers = progressHandlers;
@@ -3003,6 +2955,13 @@ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol
};
};
exports.resetBrush = function (brushId) {
exports.onInputChange(brushId, null);
imageOutputBinding.find(document).trigger("shiny-internal:brushed", {
brushId: brushId, outputId: null
});
};
//---------------------------------------------------------------------
// Source file: ../srcjs/output_binding_html.js
@@ -3033,6 +2992,8 @@ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol
// inputs/outputs. `content` can be null, a string, or an object with
// properties 'html' and 'deps'.
exports.renderContent = function (el, content) {
var where = arguments.length <= 2 || arguments[2] === undefined ? "replace" : arguments[2];
exports.unbindAll(el);
var html;
@@ -3046,15 +3007,32 @@ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol
dependencies = content.deps || [];
}
exports.renderHtml(html, el, dependencies);
exports.initializeInputs(el);
exports.bindAll(el);
exports.renderHtml(html, el, dependencies, where);
var scope = el;
if (where === "replace") {
exports.initializeInputs(el);
exports.bindAll(el);
} else {
var $parent = $(el).parent();
if ($parent.length > 0) {
scope = $parent;
if (where === "beforeBegin" || where === "afterEnd") {
var $grandparent = $parent.parent();
if ($grandparent.length > 0) scope = $grandparent;
}
}
exports.initializeInputs(scope);
exports.bindAll(scope);
}
};
// Render HTML in a DOM element, inserting singletons into head as needed
exports.renderHtml = function (html, el, dependencies) {
var where = arguments.length <= 3 || arguments[3] === undefined ? 'replace' : arguments[3];
renderDependencies(dependencies);
return singletons.renderHtml(html, el);
return singletons.renderHtml(html, el, where);
};
var htmlDependencies = {};
@@ -3123,11 +3101,15 @@ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol
var singletons = {
knownSingletons: {},
renderHtml: function renderHtml(html, el) {
renderHtml: function renderHtml(html, el, where) {
var processed = this._processHtml(html);
this._addToHead(processed.head);
this.register(processed.singletons);
$(el).html(processed.html);
if (where === "replace") {
$(el).html(processed.html);
} else {
el.insertAdjacentHTML(where, processed.html);
}
return processed;
},
// Take an object where keys are names of singletons, and merges it into
@@ -4960,7 +4942,10 @@ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol
// Iterate over all input objects for this binding
for (var j = 0; j < inputObjects.length; j++) {
binding.initialize(inputObjects[j]);
if (!inputObjects[j]._shiny_initialized) {
inputObjects[j]._shiny_initialized = true;
binding.initialize(inputObjects[j]);
}
}
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -4,9 +4,9 @@
\alias{insertUI}
\title{Insert UI objects}
\usage{
insertUI(selector, multiple = FALSE, where = c("beforeBegin", "afterBegin",
"beforeEnd", "afterEnd"), ui, immediate = FALSE, container = if (inline)
"span" else "div", inline = FALSE, session = getDefaultReactiveDomain())
insertUI(selector, where = c("beforeBegin", "afterBegin", "beforeEnd",
"afterEnd"), ui, multiple = FALSE, immediate = FALSE,
session = getDefaultReactiveDomain())
}
\arguments{
\item{selector}{A string that is accepted by jQuery's selector (i.e. the
@@ -14,11 +14,6 @@ string \code{s} to be placed in a \code{$(s)} jQuery call). This selector
will determine the element(s) relative to which you want to insert your
UI object.}
\item{multiple}{In case your selector matches more than one element,
\code{multiple} determines whether Shiny should insert the UI object
relative to all matched elements or just relative to the first
matched element (default).}
\item{where}{Where your UI object should go relative to the selector:
\describe{
\item{\code{beforeBegin}}{Before the selector element itself}
@@ -32,18 +27,22 @@ Adapted from
\href{https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML}{here}.}
\item{ui}{The UI object you want to insert. This can be anything that
you usually put inside your apps's \code{ui} function.}
you usually put inside your apps's \code{ui} function. If you're inserting
multiple elements in one call, make sure to wrap them in either a
\code{tagList()} or a \code{tags$div()} (the latter option has the
advantage that you can give it an \code{id} to make it easier to
reference or remove it later on). If you want to insert raw html, use
\code{ui = HTML()}.}
\item{multiple}{In case your selector matches more than one element,
\code{multiple} determines whether Shiny should insert the UI object
relative to all matched elements or just relative to the first
matched element (default).}
\item{immediate}{Whether the UI object should be immediately inserted into
the app when you call \code{insertUI}, or whether Shiny should wait until
all outputs have been updated and all observers have been run (default).}
\item{container}{A function to generate an HTML element to contain the UI
object.}
\item{inline}{Use an inline (\code{span()}) or block container (\code{div()},
default) for the output.}
\item{session}{The shiny session within which to call \code{insertUI}.}
}
\description{
@@ -59,12 +58,6 @@ the ones already there (all independent from one another). To
update a part of the UI (ex: an input object), you must use the
appropriate \code{render} function or a customized \code{reactive}
function. To remove any part of your UI, use \code{\link{removeUI}}.
Note that whatever UI object you pass through \code{ui}, it is always
wrapped in an extra \code{div} (or if \code{inline = TRUE}, a
\code{span}) before making its way into the DOM. This does not affect
what you mean to do, and it makes it easier to remove the whole UI
object using \code{\link{removeUI}} (if you wish to do so, of course).
}
\examples{
## Only run this example in interactive R sessions

View File

@@ -12,7 +12,9 @@ removeUI(selector, multiple = FALSE, immediate = FALSE,
string \code{s} to be placed in a \code{$(s)} jQuery call). This selector
will determine the element(s) to be removed. If you want to remove a
Shiny input or output, note that many of these are wrapped in \code{div}s,
so you may need to use a somewhat complex selector (see the Examples below).}
so you may need to use a somewhat complex selector -- see the Examples below.
(Alternatively, you could also wrap the inputs/outputs that you want to be
able to remove easily in a \code{div} with an id.)}
\item{multiple}{In case your selector matches more than one element,
\code{multiple} determines whether Shiny should remove all the matched

View File

@@ -4,6 +4,18 @@
\alias{session}
\title{Session object}
\value{
\item{allowReconnect(value)}{
If \code{value} is \code{TRUE} and run in a hosting environment (Shiny
Server or Connect) with reconnections enabled, then when the session ends
due to the network connection closing, the client will attempt to
reconnect to the server. If a reconnection is successful, the browser will
send all the current input values to the new session on the server, and
the server will recalculate any outputs and send them back to the client.
If \code{value} is \code{FALSE}, reconnections will be disabled (this is
the default state). If \code{"force"}, then the client browser will always
attempt to reconnect. The only reason to use \code{"force"} is for testing
on a local connection (without Shiny Server or Connect).
}
\item{clientData}{
A \code{\link{reactiveValues}} object that contains information about the client.
\itemize{
@@ -38,6 +50,11 @@
\item{isClosed()}{A function that returns \code{TRUE} if the client has
disconnected.
}
\item{ns(id)}{
Server-side version of \code{ns <- \link{NS}(id)}. If bare IDs need to be
explicitly namespaced for the current module, \code{session$ns("name")}
will return the fully-qualified ID.
}
\item{onEnded(callback)}{
Synonym for \code{onSessionEnded}.
}
@@ -85,17 +102,9 @@
This is the request that was used to initiate the websocket connection
(as opposed to the request that downloaded the web page for the app).
}
\item{allowReconnect(value)}{
If \code{value} is \code{TRUE} and run in a hosting environment (Shiny
Server or Connect) with reconnections enabled, then when the session ends
due to the network connection closing, the client will attempt to
reconnect to the server. If a reconnection is successful, the browser will
send all the current input values to the new session on the server, and
the server will recalculate any outputs and send them back to the client.
If \code{value} is \code{FALSE}, reconnections will be disabled (this is
the default state). If \code{"force"}, then the client browser will always
attempt to reconnect. The only reason to use \code{"force"} is for testing
on a local connection (without Shiny Server or Connect).
\item{resetBrush(brushId)}{
Resets/clears the brush with the given \code{brushId}, if it exists on
any \code{imageOutput} or \code{plotOutput} in the app.
}
\item{sendCustomMessage(type, message)}{
Sends a custom message to the web page. \code{type} must be a
@@ -117,11 +126,6 @@
from Shiny apps, but through friendlier wrapper functions like
\code{\link{updateTextInput}}.
}
\item{ns(id)}{
Server-side version of \code{ns <- \link{NS}(id)}. If bare IDs need to be
explicitly namespaced for the current module, \code{session$ns("name")}
will return the fully-qualified ID.
}
}
\description{
Shiny server functions can optionally include \code{session} as a parameter

View File

@@ -225,7 +225,10 @@ function initShiny() {
// Iterate over all input objects for this binding
for (var j = 0; j < inputObjects.length; j++) {
binding.initialize(inputObjects[j]);
if (!inputObjects[j]._shiny_initialized) {
inputObjects[j]._shiny_initialized = true;
binding.initialize(inputObjects[j]);
}
}
}
}

View File

@@ -24,7 +24,7 @@ var renderDependencies = exports.renderDependencies = function(dependencies) {
// Render HTML in a DOM element, add dependencies, and bind Shiny
// inputs/outputs. `content` can be null, a string, or an object with
// properties 'html' and 'deps'.
exports.renderContent = function(el, content) {
exports.renderContent = function(el, content, where="replace") {
exports.unbindAll(el);
var html;
@@ -38,15 +38,30 @@ exports.renderContent = function(el, content) {
dependencies = content.deps || [];
}
exports.renderHtml(html, el, dependencies);
exports.initializeInputs(el);
exports.bindAll(el);
exports.renderHtml(html, el, dependencies, where);
var scope = el;
if (where === "replace") {
exports.initializeInputs(el);
exports.bindAll(el);
} else {
var $parent = $(el).parent();
if ($parent.length > 0) {
scope = $parent;
if (where === "beforeBegin" || where === "afterEnd") {
var $grandparent = $parent.parent();
if ($grandparent.length > 0) scope = $grandparent;
}
}
exports.initializeInputs(scope);
exports.bindAll(scope);
}
};
// Render HTML in a DOM element, inserting singletons into head as needed
exports.renderHtml = function(html, el, dependencies) {
exports.renderHtml = function(html, el, dependencies, where = 'replace') {
renderDependencies(dependencies);
return singletons.renderHtml(html, el);
return singletons.renderHtml(html, el, where);
};
var htmlDependencies = {};
@@ -120,11 +135,15 @@ function renderDependency(dep) {
var singletons = {
knownSingletons: {},
renderHtml: function(html, el) {
renderHtml: function(html, el, where) {
var processed = this._processHtml(html);
this._addToHead(processed.head);
this.register(processed.singletons);
$(el).html(processed.html);
if (where === "replace") {
$(el).html(processed.html);
} else {
el.insertAdjacentHTML(where, processed.html);
}
return processed;
},
// Take an object where keys are names of singletons, and merges it into

View File

@@ -1368,3 +1368,10 @@ imageutils.createBrush = function($el, opts, coordmap, expandPixels) {
stopResizing: stopResizing
};
};
exports.resetBrush = function(brushId) {
exports.onInputChange(brushId, null);
imageOutputBinding.find(document).trigger("shiny-internal:brushed", {
brushId: brushId, outputId: null
});
};

View File

@@ -641,21 +641,19 @@ var ShinyApp = function() {
});
addMessageHandler('shiny-insert-ui', function (message) {
var targets = $(message.selector);
if (targets.length === 0) {
// render the HTML and deps to a null target, so
// the side-effect of rendering the deps, singletons,
// and <head> still occur
exports.renderHtml($([]), message.content.html, message.content.deps);
} else {
targets.each(function (i, target) {
var container = document.createElement(message.container);
insertAdjacentElement(message.where, target, container);
exports.renderContent(container, message.content);
return message.multiple;
});
}
});
var targets = $(message.selector);
if (targets.length === 0) {
// render the HTML and deps to a null target, so
// the side-effect of rendering the deps, singletons,
// and <head> still occur
exports.renderHtml($([]), message.content.html, message.content.deps);
} else {
targets.each(function (i, target) {
exports.renderContent(target, message.content, message.where);
return message.multiple;
});
}
});
addMessageHandler('shiny-remove-ui', function (message) {
var els = $(message.selector);
@@ -673,6 +671,9 @@ var ShinyApp = function() {
window.history.replaceState(null, null, message.url);
});
addMessageHandler("resetBrush", function(message) {
exports.resetBrush(message.brushId);
});
// Progress reporting ====================================================
@@ -685,36 +686,32 @@ var ShinyApp = function() {
binding.showProgress(true);
}
},
// Open a page-level progress bar
open: function(message) {
// Add progress container (for all progress items) if not already present
var $container = $('.shiny-progress-container');
if ($container.length === 0) {
$container = $('<div class="shiny-progress-container"></div>');
$('body').append($container);
}
// Add div for just this progress ID
var depth = $('.shiny-progress.open').length;
var $progress = $(progressHandlers.progressHTML);
$progress.attr('id', message.id);
$container.append($progress);
// Stack bars
var $progressBar = $progress.find('.progress');
$progressBar.css('top', depth * $progressBar.height() + 'px');
// Stack text objects
var $progressText = $progress.find('.progress-text');
$progressText.css('top', 3 * $progressBar.height() +
depth * $progressText.outerHeight() + 'px');
$progress.hide();
// Progress bar starts hidden; will be made visible if a value is provided
// during updates.
exports.notifications.show({
html:
`<div id="shiny-progress-${message.id}" class="shiny-progress">` +
'<div class="progress progress-striped active" style="display: none;"><div class="progress-bar"></div></div>' +
'<div class="progress-text">' +
'<span class="progress-message">message</span> ' +
'<span class="progress-detail"></span>' +
'</div>' +
'</div>',
id: message.id,
duration: null
});
},
// Update page-level progress bar
update: function(message) {
var $progress = $('#' + message.id + '.shiny-progress');
var $progress = $('#shiny-progress-' + message.id);
if ($progress.length === 0)
return;
if (typeof(message.message) !== 'undefined') {
$progress.find('.progress-message').text(message.message);
}
@@ -724,40 +721,18 @@ var ShinyApp = function() {
if (typeof(message.value) !== 'undefined') {
if (message.value !== null) {
$progress.find('.progress').show();
$progress.find('.bar').width((message.value*100) + '%');
}
else {
$progress.find('.progress-bar').width((message.value*100) + '%');
} else {
$progress.find('.progress').hide();
}
}
$progress.fadeIn();
},
// Close page-level progress bar
close: function(message) {
var $progress = $('#' + message.id + '.shiny-progress');
$progress.removeClass('open');
$progress.fadeOut({
complete: function() {
$progress.remove();
// If this was the last shiny-progress, remove container
if ($('.shiny-progress').length === 0)
$('.shiny-progress-container').remove();
}
});
},
// The 'bar' class is needed for backward compatibility with Bootstrap 2.
progressHTML: '<div class="shiny-progress open">' +
'<div class="progress progress-striped active"><div class="progress-bar bar"></div></div>' +
'<div class="progress-text">' +
'<span class="progress-message">message</span>' +
'<span class="progress-detail"></span>' +
'</div>' +
'</div>'
exports.notifications.remove(message.id);
}
};
exports.progressHandlers = progressHandlers;

View File

@@ -201,28 +201,3 @@ function mergeSort(list, sortfunc) {
var $escape = exports.$escape = function(val) {
return val.replace(/([!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~])/g, '\\$1');
};
// Helper function for addMessageHandler('shiny-insert-ui').
// Turns out that Firefox does not support insertAdjacentElement().
// So we have to implement our own version for insertUI.
function insertAdjacentElement(where, element, content) {
switch (where) {
case 'beforeBegin':
element.parentNode.insertBefore(content, element);
break;
case 'afterBegin':
element.insertBefore(content, element.firstChild);
break;
case 'beforeEnd':
element.appendChild(content);
break;
case 'afterEnd':
if (element.nextSibling) {
element.parentNode.insertBefore(content, element.nextSibling);
} else {
element.parentNode.appendChild(content);
}
break;
}
}

View File

@@ -819,6 +819,66 @@ test_that("observers autodestroy (or not)", {
})
})
test_that("observers are garbage collected when destroyed", {
domain <- createMockDomain()
rv <- reactiveValues(x = 1)
# Auto-destroy. GC on domain end.
a <- observe(rv$x, domain = domain)
# No auto-destroy. GC with rv.
b <- observe(rv$x, domain = domain, autoDestroy = FALSE)
# No auto-destroy and no reactive dependencies. GC immediately.
c <- observe({}, domain = domain)
c$setAutoDestroy(FALSE)
# Similar to b, but we'll set it to autoDestroy later.
d <- observe(rv$x, domain = domain, autoDestroy = FALSE)
# Like a, but we'll destroy it immediately.
e <- observe(rx$x, domain = domain)
e$destroy()
collected <- new.env(parent = emptyenv())
reg.finalizer(a, function(o) collected$a <- TRUE)
reg.finalizer(b, function(o) collected$b <- TRUE)
reg.finalizer(c, function(o) collected$c <- TRUE)
reg.finalizer(d, function(o) collected$d <- TRUE)
reg.finalizer(e, function(o) collected$e <- TRUE)
rm(list = c("a", "b", "c", "e")) # Not "d"
gc()
# Nothing can be GC'd yet, because all of the observers are
# pending execution (i.e. waiting for flushReact).
expect_equal(ls(collected), character())
flushReact()
# Now "c" can be garbage collected, because it ran and took
# no dependencies (and isn't tied to the session in any way).
# And "e" can also be garbage collected, it's been destroyed.
gc()
expect_equal(ls(collected), c("c", "e"))
domain$end()
# We can GC "a" as well; even though it references rv, it is
# destroyed when the session ends.
gc()
expect_equal(sort(ls(collected)), c("a", "c", "e"))
# It's OK to turn on auto-destroy even after the session was
# destroyed.
d$setAutoDestroy(TRUE)
# This should no-op.
d$setAutoDestroy(FALSE)
rm(d)
gc()
expect_equal(sort(ls(collected)), c("a", "c", "d", "e"))
rm(rv)
# Both rv and "b" can now be collected.
gc()
expect_equal(sort(ls(collected)), c("a", "b", "c", "d", "e"))
})
test_that("maskReactiveContext blocks use of reactives", {
vals <- reactiveValues(x = 123)