Compare commits

..

2 Commits

Author SHA1 Message Date
Barret Schloerke
2acaea1444 Add debug statements 2020-10-14 16:05:27 -04:00
Barret Schloerke
1120cfdfd7 Default to list(), not NULL 2020-10-14 16:05:20 -04:00
96 changed files with 2715 additions and 5131 deletions

View File

@@ -1,7 +1,7 @@
Package: shiny
Type: Package
Title: Web Application Framework for R
Version: 1.5.0.9005
Version: 1.5.0.9004
Authors@R: c(
person("Winston", "Chang", role = c("aut", "cre"), email = "winston@rstudio.com"),
person("Joe", "Cheng", role = "aut", email = "joe@rstudio.com"),
@@ -80,22 +80,20 @@ Imports:
mime (>= 0.3),
jsonlite (>= 0.9.16),
xtable,
digest (>= 0.6.25),
digest,
htmltools (>= 0.5.0.9001),
R6 (>= 2.0),
sourcetools,
later (>= 1.0.0),
promises (>= 1.1.1.9001),
promises (>= 1.1.0),
tools,
crayon,
rlang (>= 0.4.8.9002),
rlang (>= 0.4.0),
fastmap (>= 1.0.0),
withr,
commonmark (>= 1.7),
glue (>= 1.3.2),
bslib (>= 0.2.2.9002),
cachem,
ellipsis
bootstraplib (>= 0.2.0.9001)
Suggests:
datasets,
Cairo (>= 1.5-5),
@@ -106,7 +104,7 @@ Suggests:
ggplot2,
reactlog (>= 1.0.0),
magrittr,
shinytest (>= 1.4.0.9003),
shinytest,
yaml,
future,
dygraphs,
@@ -115,20 +113,14 @@ Suggests:
sass
Remotes:
rstudio/htmltools,
rstudio/promises,
rstudio/sass,
rstudio/bslib,
rstudio/shinytest,
r-lib/cachem,
r-lib/rlang
rstudio/bootstraplib
URL: http://shiny.rstudio.com
BugReports: https://github.com/rstudio/shiny/issues
Collate:
'globals.R'
'app-state.R'
'app_template.R'
'bind-cache.R'
'bind-event.R'
'bookmark-state-local.R'
'stack.R'
'bookmark-state.R'
@@ -138,8 +130,9 @@ Collate:
'map.R'
'utils.R'
'bootstrap.R'
'cache-disk.R'
'cache-memory.R'
'cache-utils.R'
'deprecated.R'
'diagnose.R'
'fileupload.R'
'font-awesome.R'
@@ -204,7 +197,6 @@ Collate:
'test-server.R'
'test.R'
'update-input.R'
'utils-lang.R'
'viewer.R'
RoxygenNote: 7.1.1
Encoding: UTF-8

View File

@@ -25,22 +25,6 @@ S3method(as.shiny.appobj,list)
S3method(as.shiny.appobj,shiny.appobj)
S3method(as.tags,shiny.appobj)
S3method(as.tags,shiny.render.function)
S3method(bindCache,"function")
S3method(bindCache,Observer)
S3method(bindCache,default)
S3method(bindCache,reactive.cache)
S3method(bindCache,reactive.event)
S3method(bindCache,reactiveExpr)
S3method(bindCache,shiny.render.function)
S3method(bindCache,shiny.render.function.cache)
S3method(bindCache,shiny.render.function.event)
S3method(bindCache,shiny.renderPlot)
S3method(bindEvent,Observer)
S3method(bindEvent,Observer.event)
S3method(bindEvent,default)
S3method(bindEvent,reactive.event)
S3method(bindEvent,reactiveExpr)
S3method(bindEvent,shiny.render.function)
S3method(format,reactiveExpr)
S3method(format,reactiveVal)
S3method(names,reactivevalues)
@@ -66,8 +50,6 @@ export(animationOptions)
export(appendTab)
export(as.shiny.appobj)
export(basicPage)
export(bindCache)
export(bindEvent)
export(bookmarkButton)
export(bootstrapLib)
export(bootstrapPage)
@@ -199,7 +181,6 @@ export(pre)
export(prependTab)
export(printError)
export(printStackTrace)
export(quoToFunction)
export(radioButtons)
export(reactive)
export(reactiveConsole)
@@ -336,9 +317,6 @@ import(httpuv)
import(methods)
import(mime)
import(xtable)
importFrom(digest,digest)
importFrom(ellipsis,check_dots_empty)
importFrom(ellipsis,check_dots_unnamed)
importFrom(fastmap,fastmap)
importFrom(fastmap,is.key_missing)
importFrom(fastmap,key_missing)
@@ -384,27 +362,3 @@ importFrom(htmltools,validateCssUnit)
importFrom(htmltools,withTags)
importFrom(promises,"%...!%")
importFrom(promises,"%...>%")
importFrom(promises,as.promise)
importFrom(promises,is.promising)
importFrom(promises,promise)
importFrom(promises,promise_reject)
importFrom(promises,promise_resolve)
importFrom(rlang,"%||%")
importFrom(rlang,as_function)
importFrom(rlang,as_quosure)
importFrom(rlang,enexpr)
importFrom(rlang,enquo)
importFrom(rlang,enquos)
importFrom(rlang,enquos0)
importFrom(rlang,eval_tidy)
importFrom(rlang,expr)
importFrom(rlang,get_env)
importFrom(rlang,get_expr)
importFrom(rlang,inject)
importFrom(rlang,is_na)
importFrom(rlang,is_quosure)
importFrom(rlang,new_function)
importFrom(rlang,new_quosure)
importFrom(rlang,pairlist2)
importFrom(rlang,quo)
importFrom(rlang,zap_srcref)

View File

@@ -54,12 +54,6 @@ shiny 1.5.0.9000
* `shinyOptions()` now has session-level scoping, in addition to global and application-level scoping. (#3080)
* `runApp()` now warns when running an application in an R package directory. (#3114)
* Shiny now uses `cache_mem` from the cachem package, instead of `memoryCache` and `diskCache`. (#3118)
* Closed #3140: Added support for `...` argument in `icon()`. (#3143)
### Bug fixes
* Fixed #2859: `renderPlot()` wasn't correctly setting `showtext::showtext_opts()`'s `dpi` setting with the correct resolution on high resolution displays; which means, if the font was rendered by showtext, font sizes would look smaller than they should on such displays. (#2941)

View File

@@ -1,757 +0,0 @@
utils::globalVariables(".GenericCallEnv", add = TRUE)
#' Add caching with reactivity to an object
#'
#' @description
#'
#' `bindCache()` adds caching [reactive()] expressions and `render*` functions
#' (like [renderText()], [renderTable()], ...).
#'
#' Ordinary [reactive()] expressions automatically cache their _most recent_
#' value, which helps to avoid redundant computation in downstream reactives.
#' `bindCache()` will cache all previous values (as long as they fit in the
#' cache) and they can be shared across user sessions. This allows
#' `bindCache()` to dramatically improve performance when used correctly.
#' @details
#'
#' `bindCache()` requires one or more expressions that are used to generate a
#' **cache key**, which is used to determine if a computation has occurred
#' before and hence can be retrieved from the cache. If you're familiar with the
#' concept of memoizing pure functions (e.g., the \pkg{memoise} package), you
#' can think of the cache key as the input(s) to a pure function. As such, one
#' should take care to make sure the use of `bindCache()` is _pure_ in the same
#' sense, namely:
#'
#' 1. For a given key, the return value is always the same.
#' 2. Evaluation has no side-effects.
#'
#' In the example here, the `bindCache()` key consists of `input$x` and
#' `input$y` combined, and the value is `input$x * input$y`. In this simple
#' example, for any given key, there is only one possible returned value.
#'
#' ```
#' r <- reactive({ input$x * input$y }) %>%
#' bindCache(input$x, input$y)
#' ```
#'
#' The largest performance improvements occur when the cache key is fast to
#' compute and the reactive expression is slow to compute. To see if the value
#' should be computed, a cached reactive evaluates the key, and then serializes
#' and hashes the result. If the resulting hashed key is in the cache, then the
#' cached reactive simply retrieves the previously calculated value and returns
#' it; if not, then the value is computed and the result is stored in the cache
#' before being returned.
#'
#' To compute the cache key, `bindCache()` hashes the contents of `...`, so it's
#' best to avoid including large objects in a cache key since that can result in
#' slow hashing. It's also best to avoid reference objects like environments and
#' R6 objects, since the serialization of these objects may not capture relevant
#' changes.
#'
#' If you want to use a large object as part of a cache key, it may make sense
#' to do some sort of reduction on the data that still captures information
#' about whether a value can be retrieved from the cache. For example, if you
#' have a large data set with timestamps, it might make sense to extract the
#' most recent timestamp and return that. Then, instead of hashing the entire
#' data object, the cached reactive only needs to hash the timestamp.
#'
#' ```
#' r <- reactive({ compute(bigdata()) } %>%
#' bindCache({ extract_most_recent_time(bigdata()) })
#' ```
#'
#' For computations that are vert slow, it often makes sense to pair
#' [bindCache()] with [bindEvent()] so that no computation is performed until
#' the user explicitly requests it (for more, see the Details section of
#' [bindEvent()]).
#' @section Cache keys and reactivity:
#'
#' Because the **value** expression (from the original [reactive()]) is
#' cached, it is not necessarily re-executed when someone retrieves a value,
#' and therefore it can't be used to decide what objects to take reactive
#' dependencies on. Instead, the **key** is used to figure out which objects
#' to take reactive dependencies on. In short, the key expression is reactive,
#' and value expression is no longer reactive.
#'
#' Here's an example of what not to do: if the key is `input$x` and the value
#' expression is from `reactive({input$x + input$y})`, then the resulting
#' cached reactive will only take a reactive dependency on `input$x` -- it
#' won't recompute `{input$x + input$y}` when just `input$y` changes.
#' Moreover, the cache won't use `input$y` as part of the key, and so it could
#' return incorrect values in the future when it retrieves values from the
#' cache. (See the examples below for an example of this.)
#'
#' A better cache key would be something like `input$x, input$y`. This does
#' two things: it ensures that a reactive dependency is taken on both
#' `input$x` and `input$y`, and it also makes sure that both values are
#' represented in the cache key.
#'
#' In general, `key` should use the same reactive inputs as `value`, but the
#' computation should be simpler. If there are other (non-reactive) values
#' that are consumed, such as external data sources, they should be used in
#' the `key` as well. Note that if the `key` is large, it can make sense to do
#' some sort of reduction on it so that the serialization and hashing of the
#' cache key is not too expensive.
#'
#' Remember that the key is _reactive_, so it is not re-executed every single
#' time that someone accesses the cached reactive. It is only re-executed if
#' it has been invalidated by one of the reactives it depends on. For
#' example, suppose we have this cached reactive:
#'
#' ```
#' r <- reactive({ input$x * input$y }) %>%
#' bindCache(input$x, input$y)
#' ```
#'
#' In this case, the key expression is essentially `reactive(list(input$x,
#' input$y))` (there's a bit more to it, but that's a good enough
#' approximation). The first time `r()` is called, it executes the key, then
#' fails to find it in the cache, so it executes the value expression, `{
#' input$x + input$y }`. If `r()` is called again, then it does not need to
#' re-execute the key expression, because it has not been invalidated via a
#' change to `input$x` or `input$y`; it simply returns the previous value.
#' However, if `input$x` or `input$y` changes, then the reactive expression will
#' be invalidated, and the next time that someone calls `r()`, the key
#' expression will need to be re-executed.
#'
#' Note that if the cached reactive is passed to [bindEvent()], then the key
#' expression will no longer be reactive; instead, the event expression will be
#' reactive.
#'
#'
#' @section Cache scope:
#'
#' By default, when `bindCache()` is used, it is scoped to the running
#' application. That means that it shares a cache with all user sessions
#' connected to the application (within the R process). This is done with the
#' `cache` parameter's default value, `"app"`.
#'
#' With an app-level cache scope, one user can benefit from the work done for
#' another user's session. In most cases, this is the best way to get
#' performance improvements from caching. However, in some cases, this could
#' leak information between sessions. For example, if the cache key does not
#' fully encompass the inputs used by the value, then data could leak between
#' the sessions. Or if a user sees that a cached reactive returns its value
#' very quickly, they may be able to infer that someone else has already used
#' it with the same values.
#'
#' It is also possible to scope the cache to the session, with
#' `cache="session"`. This removes the risk of information leaking between
#' sessions, but then one session cannot benefit from computations performed in
#' another session.
#'
#' It is possible to pass in caching objects directly to
#' `bindCache()`. This can be useful if, for example, you want to use a
#' particular type of cache with specific cached reactives, or if you want to
#' use a [cachem::cache_disk()] that is shared across multiple processes and
#' persists beyond the current R session.
#'
#' To use different settings for an application-scoped cache, you can call
#' [shinyOptions()] at the top of your app.R, server.R, or
#' global.R. For example, this will create a cache with 500 MB of space
#' instead of the default 200 MB:
#'
#' ```
#' shinyOptions(cache = cachem::cache_mem(size = 500e6))
#' ```
#'
#' To use different settings for a session-scoped cache, you can set
#' `self$cache` at the top of your server function. By default, it will create
#' a 200 MB memory cache for each session, but you can replace it with
#' something different. To use the session-scoped cache, you must also call
#' `bindCache()` with `cache="session"`. This will create a 100 MB cache for
#' the session:
#'
#' ```
#' function(input, output, session) {
#' session$cache <- cachem::cache_mem(size = 100e6)
#' ...
#' }
#' ```
#'
#' If you want to use a cache that is shared across multiple R processes, you
#' can use a [cachem::cache_disk()]. You can create a application-level shared
#' cache by putting this at the top of your app.R, server.R, or global.R:
#'
#' ```
#' shinyOptions(cache = cachem::cache_disk(file.path(dirname(tempdir()), "myapp-cache"))
#' ```
#'
#' This will create a subdirectory in your system temp directory named
#' `myapp-cache` (replace `myapp-cache` with a unique name of
#' your choosing). On most platforms, this directory will be removed when
#' your system reboots. This cache will persist across multiple starts and
#' stops of the R process, as long as you do not reboot.
#'
#' To have the cache persist even across multiple reboots, you can create the
#' cache in a location outside of the temp directory. For example, it could
#' be a subdirectory of the application:
#'
#' ```
#' shinyOptions(cache = cachem::cache_disk("./myapp-cache"))
#' ```
#'
#' In this case, resetting the cache will have to be done manually, by deleting
#' the directory.
#'
#' You can also scope a cache to just one item, or selected items. To do that,
#' create a [cachem::cache_mem()] or [cachem::cache_disk()], and pass it
#' as the `cache` argument of `bindCache()`.
#'
#'
#' @section Computing cache keys:
#'
#' The actual cache key that is used internally takes value from evaluating
#' the key expression(s) (from the `...` arguments) and combines it with the
#' (unevaluated) value expression.
#'
#' This means that if there are two cached reactives which have the same
#' result from evaluating the key, but different value expressions, then they
#' will not need to worry about collisions.
#'
#' However, if two cached reactives have identical key and value expressions
#' expressions, they will share the cached values. This is useful when using
#' `cache="app"`: there may be multiple user sessions which create separate
#' cached reactive objects (because they are created from the same code in the
#' server function, but the server function is executed once for each user
#' session), and those cached reactive objects across sessions can share
#' values in the cache.
#'
#' @section Async with cached reactives:
#'
#' With a cached reactive expression, the key and/or value expression can be
#' _asynchronous_. In other words, they can be promises --- not regular R
#' promises, but rather objects provided by the
#' \href{https://rstudio.github.io/promises/}{\pkg{promises}} package, which
#' are similar to promises in JavaScript. (See [promises::promise()] for more
#' information.) You can also use [future::future()] objects to run code in a
#' separate process or even on a remote machine.
#'
#' If the value returns a promise, then anything that consumes the cached
#' reactive must expect it to return a promise.
#'
#' Similarly, if the key is a promise (in other words, if it is asynchronous),
#' then the entire cached reactive must be asynchronous, since the key must be
#' computed asynchronously before it knows whether to compute the value or the
#' value is retrieved from the cache. Anything that consumes the cached
#' reactive must therefore expect it to return a promise.
#'
#'
#' @section Developing render functions for caching:
#'
#' If you've implemented your own `render*()` function, you may need to
#' provide a `cacheHint` to [createRenderFunction()] (or
#' [htmlwidgets::shinyRenderWidget()], if you've authored an htmlwidget) in
#' order for `bindCache()` to correctly compute a cache key.
#'
#' The potential problem is a cache collision. Consider the following:
#'
#' ```
#' output$x1 <- renderText({ input$x }) %>% bindCache(input$x)
#' output$x2 <- renderText({ input$x * 2 }) %>% bindCache(input$x)
#' ```
#'
#' Both `output$x1` and `output$x2` use `input$x` as part of their cache key,
#' but if it were the only thing used in the cache key, then the two outputs
#' would have a cache collision, and they would have the same output. To avoid
#' this, a _cache hint_ is automatically added when [renderText()] calls
#' [createRenderFunction()]. The cache hint is used as part of the actual
#' cache key, in addition to the one passed to `bindCache()` by the user. The
#' cache hint can be viewed by calling the internal Shiny function
#' `extractCacheHint()`:
#'
#' ```
#' r <- renderText({ input$x })
#' shiny:::extractCacheHint(r)
#' ```
#'
#' This returns a nested list containing an item, `$origUserFunc$body`, which
#' in this case is the expression which was passed to `renderText()`:
#' `{ input$x }`. This (quoted) expression is mixed into the actual cache
#' key, and it is how `output$x1` does not have collisions with `output$x2`.
#'
#' For most developers of render functions, nothing extra needs to be done;
#' the automatic inference of the cache hint is sufficient. Again, you can
#' check it by calling `shiny:::extractCacheHint()`, and by testing the
#' render function for cache collisions in a real application.
#'
#' In some cases, however, the automatic cache hint inference is not
#' sufficient, and it is necessary to provide a cache hint. This is true
#' for `renderPrint()`. Unlike `renderText()`, it wraps the user-provided
#' expression in another function, before passing it to [markRenderFunction()]
#' (instead of [createRenderFunction()]). Because the user code is wrapped in
#' another function, markRenderFunction() is not able to automatically extract
#' the user-provided code and use it in the cache key. Instead, `renderPrint`
#' calls `markRenderFunction()`, it explicitly passes along a `cacheHint`,
#' which includes a label and the original user expression.
#'
#' In general, if you need to provide a `cacheHint`, it is best practice to
#' provide a `label` id, the user's `expr`, as well as any other arguments
#' that may influence the final value.
#'
#' For \pkg{htmlwidgets}, it will try to automatically infer a cache hint;
#' again, you can inspect the cache hint with `shiny:::extractCacheHint()` and
#' also test it in an application. If you do need to explicitly provide a
#' cache hint, pass it to `shinyRenderWidget`. For example:
#'
#' ```
#' renderMyWidget <- function(expr) {
#' expr <- substitute(expr)
#'
#' htmlwidgets::shinyRenderWidget(expr,
#' myWidgetOutput,
#' quoted = TRUE,
#' env = parent.frame(),
#' cacheHint = list(label = "myWidget", userExpr = expr)
#' )
#' }
#' ```
#'
#'
#' @section Uncacheable objects:
#'
#' Some render functions cannot be cached, typically because they have side
#' effects or modify some external state, and they must re-execute each time
#' in order to work properly.
#'
#' For developers of such code, they should call [createRenderFunction()] or
#' [markRenderFunction()] with `cacheHint = FALSE`.
#'
#'
#' @section Caching with `renderPlot()`:
#'
#' When `bindCache()` is used with `renderPlot()`, the `height` and `width`
#' passed to the original `renderPlot()` are ignored. They are superseded by
#' `sizePolicy` argument passed to `bindCache. The default is:
#'
#' ```
#' sizePolicy = sizeGrowthRatio(width = 400, height = 400, growthRate = 1.2)
#' ```
#'
#' `sizePolicy` must be a function that takes a two-element numeric vector as
#' input, representing the width and height of the `<img>` element in the
#' browser window, and it must return a two-element numeric vector, representing
#' the pixel dimensions of the plot to generate. The purpose is to round the
#' actual pixel dimensions from the browser to some other dimensions, so that
#' this will not generate and cache images of every possible pixel dimension.
#' See [sizeGrowthRatio()] for more information on the default sizing policy.
#'
#' @param x The object to add caching to.
#' @param ... One or more expressions to use in the caching key.
#' @param cache The scope of the cache, or a cache object. This can be `"app"`
#' (the default), `"session"`, or a cache object like a
#' [cachem::cache_disk()]. See the Cache Scoping section for more information.
#'
#' @seealso [bindEvent()], [renderCachedPlot()] for caching plots.
#'
#' @examples
#' \dontrun{
#' rc <- bindCache(
#' x = reactive({
#' Sys.sleep(2) # Pretend this is expensive
#' input$x * 100
#' }),
#' input$x
#' )
#'
#' # Can make it prettier with the %>% operator
#' library(magrittr)
#'
#' rc <- reactive({
#' Sys.sleep(2)
#' input$x * 100
#' }) %>%
#' bindCache(input$x)
#'
#' }
#'
#' ## Only run app examples in interactive R sessions
#' if (interactive()) {
#'
#' # Basic example
#' shinyApp(
#' ui = fluidPage(
#' sliderInput("x", "x", 1, 10, 5),
#' sliderInput("y", "y", 1, 10, 5),
#' div("x * y: "),
#' verbatimTextOutput("txt")
#' ),
#' server = function(input, output) {
#' r <- reactive({
#' # The value expression is an _expensive_ computation
#' message("Doing expensive computation...")
#' Sys.sleep(2)
#' input$x * input$y
#' }) %>%
#' bindCache(input$x, input$y)
#'
#' output$txt <- renderText(r())
#' }
#' )
#'
#'
#' # Caching renderText
#' shinyApp(
#' ui = fluidPage(
#' sliderInput("x", "x", 1, 10, 5),
#' sliderInput("y", "y", 1, 10, 5),
#' div("x * y: "),
#' verbatimTextOutput("txt")
#' ),
#' server = function(input, output) {
#' output$txt <- renderText({
#' message("Doing expensive computation...")
#' Sys.sleep(2)
#' input$x * input$y
#' }) %>%
#' bindCache(input$x, input$y)
#' }
#' )
#'
#'
#' # Demo of using events and caching with an actionButton
#' shinyApp(
#' ui = fluidPage(
#' sliderInput("x", "x", 1, 10, 5),
#' sliderInput("y", "y", 1, 10, 5),
#' actionButton("go", "Go"),
#' div("x * y: "),
#' verbatimTextOutput("txt")
#' ),
#' server = function(input, output) {
#' r <- reactive({
#' message("Doing expensive computation...")
#' Sys.sleep(2)
#' input$x * input$y
#' }) %>%
#' bindCache(input$x, input$y) %>%
#' bindEvent(input$go)
#' # The cached, eventified reactive takes a reactive dependency on
#' # input$go, but doesn't use it for the cache key. It uses input$x and
#' # input$y for the cache key, but doesn't take a reactive depdency on
#' # them, because the reactive dependency is superseded by addEvent().
#'
#' output$txt <- renderText(r())
#' }
#' )
#'
#' }
#'
#' @export
bindCache <- function(x, ..., cache = "app") {
force(cache)
UseMethod("bindCache")
}
#' @export
bindCache.default <- function(x, ...) {
stop("Don't know how to handle object with class ", paste(class(x), collapse = ", "))
}
#' @export
bindCache.reactiveExpr <- function(x, ..., cache = "app") {
check_dots_unnamed()
label <- exprToLabel(substitute(key), "cachedReactive")
domain <- reactive_get_domain(x)
# Convert the ... to a function that returns their evaluated values.
keyFunc <- quos_to_func(enquos0(...))
valueFunc <- reactive_get_value_func(x)
# Hash cache hint now -- this will be added to the key later on, to reduce the
# chance of key collisions with other cachedReactives.
cacheHint <- digest(extractCacheHint(x), algo = "spookyhash")
valueFunc <- wrapFunctionLabel(valueFunc, "cachedReactiveValueFunc", ..stacktraceon = TRUE)
# Don't hold on to the reference for x, so that it can be GC'd
rm(x)
# Hacky workaround for issue with `%>%` preventing GC:
# https://github.com/tidyverse/magrittr/issues/229
if (exists(".GenericCallEnv") && exists(".", envir = .GenericCallEnv)) {
rm(list = ".", envir = .GenericCallEnv)
}
res <- reactive(label = label, domain = domain, {
cache <- resolve_cache_object(cache, domain)
hybrid_chain(
keyFunc(),
generateCacheFun(valueFunc, cache, cacheHint, cacheReadHook = identity, cacheWriteHook = identity)
)
})
class(res) <- c("reactive.cache", class(res))
res
}
#' @export
bindCache.shiny.render.function <- function(x, ..., cache = "app") {
check_dots_unnamed()
keyFunc <- quos_to_func(enquos0(...))
cacheHint <- digest(extractCacheHint(x), algo = "spookyhash")
cacheWriteHook <- attr(x, "cacheWriteHook", exact = TRUE) %||% identity
cacheReadHook <- attr(x, "cacheReadHook", exact = TRUE) %||% identity
valueFunc <- x
renderFunc <- function(...) {
domain <- getDefaultReactiveDomain()
cache <- resolve_cache_object(cache, domain)
hybrid_chain(
keyFunc(),
generateCacheFun(valueFunc, cache, cacheHint, cacheReadHook, cacheWriteHook, ...)
)
}
renderFunc <- addAttributes(renderFunc, renderFunctionAttributes(valueFunc))
class(renderFunc) <- c("shiny.render.function.cache", class(valueFunc))
renderFunc
}
#' @export
bindCache.shiny.renderPlot <- function(x, ...,
cache = "app",
sizePolicy = sizeGrowthRatio(width = 400, height = 400, growthRate = 1.2))
{
check_dots_unnamed()
valueFunc <- x
# Given the actual width/height of the image element in the browser, the
# resize observer computes the width/height using sizePolicy() and pushes
# those values into `fitWidth` and `fitHeight`. It's done this way so that the
# `fitWidth` and `fitHeight` only change (and cause invalidations of the key
# expression) when the rendered image size changes, and not every time the
# browser's <img> tag changes size.
#
# If the key expression were invalidated every time the image element changed
# size, even if the resulting key was the same (because `sizePolicy()` gave
# the same output for a slightly different img element size), it would result
# in getting the (same) image from the cache and sending it to the client
# again. This resize observer prevents that.
fitDims <- reactiveVal(NULL)
resizeObserverCreated <- FALSE
outputName <- NULL
ensureResizeObserver <- function() {
if (resizeObserverCreated)
return()
doResizeCheck <- function() {
if (is.null(outputName)) {
outputName <<- getCurrentOutputInfo()$name
}
session <- getDefaultReactiveDomain()
width <- session$clientData[[paste0('output_', outputName, '_width')]] %||% 0
height <- session$clientData[[paste0('output_', outputName, '_height')]] %||% 0
rect <- sizePolicy(c(width, height))
fitDims(list(width = rect[1], height = rect[2]))
}
# Run it once immediately, then set up the observer
isolate(doResizeCheck())
observe({
doResizeCheck()
})
# TODO: Make sure this observer gets GC'd if output$foo is replaced.
# Currently, if you reassign output$foo, the observer persists until the
# session ends. This is generally bad programming practice and should be
# rare, but still, we should try to clean up properly.
resizeObserverCreated <<- TRUE
}
renderFunc <- function(...) {
hybrid_chain(
# Pass in fitDims so that so that the generated plot will be the
# dimensions specified by the sizePolicy; otherwise the plot would be the
# exact dimensions of the img element, which isn't what we want for cached
# plots.
valueFunc(..., get_dims = fitDims),
function(img) {
# Replace exact pixel dimensions; instead, the max-height and max-width
# will be set to 100% from CSS.
img$class <- "shiny-scalable"
img$width <- NULL
img$height <- NULL
img
}
)
}
renderFunc <- addAttributes(renderFunc, renderFunctionAttributes(valueFunc))
class(renderFunc) <- class(valueFunc)
bindCache.shiny.render.function(
renderFunc,
...,
{
ensureResizeObserver()
session <- getDefaultReactiveDomain()
if (is.null(session) || is.null(fitDims())) {
req(FALSE)
}
pixelratio <- session$clientData$pixelratio %||% 1
list(fitDims(), pixelratio)
},
cache = cache
)
}
#' @export
bindCache.reactive.cache <- function(x, ...) {
stop("bindCache() has already been called on the object.")
}
#' @export
bindCache.shiny.render.function.cache <- bindCache.reactive.cache
#' @export
bindCache.reactive.event <- function(x, ...) {
stop("Can't call bindCache() after calling bindEvent() on an object. Maybe you wanted to call bindEvent() after bindCache()?")
}
#' @export
bindCache.shiny.render.function.event <- bindCache.reactive.event
#' @export
bindCache.Observer <- function(x, ...) {
stop("Can't bindCache an observer, because observers exist for the side efects, not for their return values.")
}
#' @export
bindCache.function <- function(x, ...) {
stop(
"Don't know how to add caching to a plain function. ",
"If this is a render* function for Shiny, it may need to be updated. ",
"Please see ?shiny::bindCache for more information."
)
}
# Returns a function which should be passed as a step in to hybrid_chain(). The
# returned function takes a cache key as input and manages storing and retrieving
# values from the cache, as well as executing the valueFunc if needed.
generateCacheFun <- function(
valueFunc,
cache,
cacheHint,
cacheReadHook,
cacheWriteHook,
...
) {
function(cacheKeyResult) {
key_str <- digest(list(cacheKeyResult, cacheHint), algo = "spookyhash")
res <- cache$get(key_str)
# Case 1: cache hit
if (!is.key_missing(res)) {
return(hybrid_chain(
{
# The first step is just to convert `res` to a promise or not, so
# that hybrid_chain() knows to propagate the promise-ness.
if (res$is_promise) promise_resolve(res)
else res
},
function(res) {
if (res$error) {
stop(res$value)
}
cacheReadHook(valueWithVisible(res))
}
))
}
# Case 2: cache miss
#
# valueFunc() might return a promise, or an actual value. Normally we'd
# use a hybrid_chain() for this, but in this case, we need to have
# different behavior if it's a promise or not a promise -- the
# information about whether or not it's a promise needs to be stored in
# the cache. We need to handle both cases and record in the cache
# whether it's a promise or not, so that any consumer of the
# cachedReactive() will be given the correct kind of object (a promise
# vs. an actual value) in the case of a future cache hit.
p <- withCallingHandlers(
withVisible(isolate(valueFunc(...))),
error = function(e) {
cache$set(key_str, list(
is_promise = FALSE,
value = e,
visible = TRUE,
error = TRUE
))
}
)
if (is.promising(p$value)) {
p$value <- as.promise(p$value)
p$value <- p$value$
then(function(value) {
res <- withVisible(value)
cache$set(key_str, list(
is_promise = TRUE,
value = cacheWriteHook(res$value),
visible = res$visible,
error = FALSE
))
valueWithVisible(res)
})$
catch(function(e) {
cache$set(key_str, list(
is_promise = TRUE,
value = e,
visible = TRUE,
error = TRUE
))
stop(e)
})
valueWithVisible(p)
} else {
# result is an ordinary value, not a promise.
cache$set(key_str, list(
is_promise = FALSE,
value = cacheWriteHook(p$value),
visible = p$visible,
error = FALSE
))
return(valueWithVisible(p))
}
}
}
extractCacheHint <- function(func) {
cacheHint <- attr(func, "cacheHint", exact = TRUE)
if (is_false(cacheHint)) {
stop(
"Cannot call `bindCache()` on this object because it is marked as not cacheable.",
call. = FALSE
)
}
if (is.null(cacheHint)) {
warning("No cacheHint found for this object. ",
"Caching may not work properly.")
}
cacheHint
}

View File

@@ -1,308 +0,0 @@
#' Make an object respond only to specified reactive events
#'
#' @description
#'
#' Modify an object to respond to "event-like" reactive inputs, values, and
#' expressions. `bindEvent()` can be used with reactive expressions, render
#' functions, and observers. The resulting object takes a reactive dependency on
#' the `...` arguments, and not on the original object's code. This can, for
#' example, be used to make an observer execute only when a button is pressed.
#'
#' @section Details:
#'
#' Shiny's reactive programming framework is primarily designed for calculated
#' values (reactive expressions) and side-effect-causing actions (observers)
#' that respond to *any* of their inputs changing. That's often what is
#' desired in Shiny apps, but not always: sometimes you want to wait for a
#' specific action to be taken from the user, like clicking an
#' [actionButton()], before calculating an expression or taking an action. A
#' reactive value or expression that is used to trigger other calculations in
#' this way is called an *event*.
#'
#' These situations demand a more imperative, "event handling" style of
#' programming that is possible--but not particularly intuitive--using the
#' reactive programming primitives [observe()] and [isolate()]. `bindEvent()`
#' provides a straightforward API for event handling that wraps `observe` and
#' `isolate`.
#'
#' The `...` arguments are captured as expressions and combined into an
#' **event expression**. When this event expression is invalidated (when its
#' upstream reactive inputs change), that is an **event**, and it will cause
#' the original object's code to execute.
#'
#' Use `bindEvent()` with `observe()` whenever you want to *perform an action*
#' in response to an event. (Note that "recalculate a value" does not
#' generally count as performing an action -- use [reactive()] for that.) The
#' first argument is observer whose code should be executed whenever the event
#' occurs.
#'
#' Use `bindEvent()` with `reactive()` to create a *calculated value* that only
#' updates in response to an event. This is just like a normal [reactive
#' expression][reactive] except it ignores all the usual invalidations that
#' come from its reactive dependencies; it only invalidates in response to the
#' given event.
#'
#' `bindEvent()` is often used with [bindCache()].
#'
#' @section ignoreNULL and ignoreInit:
#'
#' `bindEvent()` takes an `ignoreNULL` parameter that affects behavior when
#' the event expression evaluates to `NULL` (or in the special case of an
#' [actionButton()], `0`). In these cases, if `ignoreNULL` is `TRUE`, then it
#' will raise a silent [validation][validate] error. This is useful behavior
#' if you don't want to do the action or calculation when your app first
#' starts, but wait for the user to initiate the action first (like a "Submit"
#' button); whereas `ignoreNULL=FALSE` is desirable if you want to initially
#' perform the action/calculation and just let the user re-initiate it (like a
#' "Recalculate" button).
#'
#' `bindEvent()` also takes an `ignoreInit` argument. By default, reactive
#' expressions and observers will run on the first reactive flush after they
#' are created (except if, at that moment, the event expression evaluates to
#' `NULL` and `ignoreNULL` is `TRUE`). But when responding to a click of an
#' action button, it may often be useful to set `ignoreInit` to `TRUE`. For
#' example, if you're setting up an observer to respond to a dynamically
#' created button, then `ignoreInit = TRUE` will guarantee that the action
#' will only be triggered when the button is actually clicked, instead of also
#' being triggered when it is created/initialized. Similarly, if you're
#' setting up a reactive that responds to a dynamically created button used to
#' refresh some data (which is then returned by that `reactive`), then you
#' should use `reactive(...) %>% bindEvent(..., ignoreInit = TRUE)` if you
#' want to let the user decide if/when they want to refresh the data (since,
#' depending on the app, this may be a computationally expensive operation).
#'
#' Even though `ignoreNULL` and `ignoreInit` can be used for similar purposes
#' they are independent from one another. Here's the result of combining
#' these:
#'
#' \describe{
#' \item{`ignoreNULL = TRUE` and `ignoreInit = FALSE`}{
#' This is the default. This combination means that reactive/observer code
#' will run every time that event expression is not
#' `NULL`. If, at the time of creation, the event expression happens
#' to *not* be `NULL`, then the code runs.
#' }
#' \item{`ignoreNULL = FALSE` and `ignoreInit = FALSE`}{
#' This combination means that reactive/observer code will
#' run every time no matter what.
#' }
#' \item{`ignoreNULL = FALSE` and `ignoreInit = TRUE`}{
#' This combination means that reactive/observer code will
#' *not* run at the time of creation (because `ignoreInit = TRUE`),
#' but it will run every other time.
#' }
#' \item{`ignoreNULL = TRUE` and `ignoreInit = TRUE`}{
#' This combination means that reactive/observer code will
#' *not* at the time of creation (because `ignoreInit = TRUE`).
#' After that, the reactive/observer code will run every time that
#' the event expression is not `NULL`.
#' }
#' }
#'
#' @section Types of objects:
#'
#' `bindEvent()` can be used with reactive expressions, observers, and shiny
#' render functions.
#'
#' When `bindEvent()` is used with `reactive()`, it creates a new reactive
#' expression object.
#'
#' When `bindEvent()` is used with `observe()`, it alters the observer in
#' place. It can only be used with observers which have not yet executed.
#'
#' @section Combining events and caching:
#'
#' In many cases, it makes sense to use `bindEvent()` along with
#' `bindCache()`, because they each can reduce the amount of work done on the
#' server. For example, you could have [sliderInput]s `x` and `y` and a
#' `reactive()` that performs a time-consuming operation with those values.
#' Using `bindCache()` can speed things up, especially if there are multiple
#' users. But it might make sense to also not do the computation until the
#' user sets both `x` and `y`, and then clicks on an [actionButton] named
#' `go`.
#'
#' To use both caching and events, the object should first be passed to
#' `bindCache()`, then `bindEvent()`. For example:
#'
#' ```
#' r <- reactive({
#' Sys.sleep(2) # Pretend this is an expensive computation
#' input$x * input$y
#' }) %>%
#' bindCache(input$x, input$y) %>%
#' bindEvent(input$go)
#' ```
#'
#' Anything that consumes `r()` will take a reactive dependency on the event
#' expression given to `bindEvent()`, and not the cache key expression given to
#' `bindCache()`. In this case, it is just `input$go`.
#'
#' @param x An object to wrap so that is triggered only when a the specified
#' event occurs.
#' @param ignoreNULL Whether the action should be triggered (or value
#' calculated) when the input is `NULL`. See Details.
#' @param ignoreInit If `TRUE`, then, when the eventified object is first
#' created/initialized, don't trigger the action or (compute the value). The
#' default is `FALSE`. See Details.
#' @param once Used only for observers. Whether this `observer` should be
#' immediately destroyed after the first time that the code in the observer is
#' run. This pattern is useful when you want to subscribe to a event that
#' should only happen once.
#' @param label A label for the observer or reactive, useful for debugging.
#' @param ... One or more expressions that represents the event; this can be a
#' simple reactive value like `input$click`, a call to a reactive expression
#' like `dataset()`, or even a complex expression inside curly braces. If
#' there are multiple expressions in the `...`, then it will take a dependency
#' on all of them.
#' @export
bindEvent <- function(x, ..., ignoreNULL = TRUE, ignoreInit = FALSE,
once = FALSE, label = NULL)
{
check_dots_unnamed()
force(ignoreNULL)
force(ignoreInit)
force(once)
UseMethod("bindEvent")
}
#' @export
bindEvent.default <- function(x, ...) {
stop("Don't know how to handle object with class ", paste(class(x), collapse = ", "))
}
#' @export
bindEvent.reactiveExpr <- function(x, ..., ignoreNULL = TRUE, ignoreInit = FALSE,
label = NULL)
{
domain <- reactive_get_domain(x)
qs <- enquos0(...)
eventFunc <- quos_to_func(qs)
valueFunc <- reactive_get_value_func(x)
valueFunc <- wrapFunctionLabel(valueFunc, "eventReactiveValueFunc", ..stacktraceon = TRUE)
label <- label %||%
sprintf('bindEvent(%s, %s)', attr(x, "observable", exact = TRUE)$.label, quos_to_label(qs))
# Don't hold on to the reference for x, so that it can be GC'd
rm(x)
initialized <- FALSE
res <- reactive(label = label, domain = domain, ..stacktraceon = FALSE, {
hybrid_chain(
eventFunc(),
function(value) {
if (ignoreInit && !initialized) {
initialized <<- TRUE
req(FALSE)
}
req(!ignoreNULL || !isNullEvent(value))
isolate(valueFunc())
}
)
})
class(res) <- c("reactive.event", class(res))
res
}
#' @export
bindEvent.shiny.render.function <- function(x, ..., ignoreNULL = TRUE, ignoreInit = FALSE) {
eventFunc <- quos_to_func(enquos0(...))
valueFunc <- x
initialized <- FALSE
renderFunc <- function(...) {
hybrid_chain(
eventFunc(),
function(value) {
if (ignoreInit && !initialized) {
initialized <<- TRUE
req(FALSE)
}
req(!ignoreNULL || !isNullEvent(value))
isolate(valueFunc(...))
}
)
}
renderFunc <- addAttributes(renderFunc, renderFunctionAttributes(valueFunc))
class(renderFunc) <- c("shiny.render.function.event", class(valueFunc))
renderFunc
}
#' @export
bindEvent.Observer <- function(x, ..., ignoreNULL = TRUE, ignoreInit = FALSE,
once = FALSE, label = NULL)
{
if (x$.execCount > 0) {
stop("Cannot call bindEvent() on an Observer that has already been executed.")
}
qs <- enquos0(...)
eventFunc <- quos_to_func(qs)
valueFunc <- x$.func
# Note that because the observer will already have been logged by this point,
# this updated label won't show up in the reactlog.
x$.label <- label %||% sprintf('bindEvent(%s, %s)', x$.label, quos_to_label(qs))
initialized <- FALSE
x$.func <- wrapFunctionLabel(
name = x$.label,
..stacktraceon = FALSE,
func = function() {
hybrid_chain(
eventFunc(),
function(value) {
if (ignoreInit && !initialized) {
initialized <<- TRUE
return()
}
if (ignoreNULL && isNullEvent(value)) {
return()
}
if (once) {
on.exit(x$destroy())
}
req(!ignoreNULL || !isNullEvent(value))
isolate(valueFunc())
}
)
}
)
class(x) <- c("Observer.event", class(x))
invisible(x)
}
#' @export
bindEvent.reactive.event <- function(x, ...) {
stop("bindEvent() has already been called on the object.")
}
#' @export
bindEvent.Observer.event <- bindEvent.reactive.event

View File

@@ -79,7 +79,7 @@ saveShinySaveState <- function(state) {
# Look for a save.interface function. This will be defined by the hosting
# environment if it supports bookmarking.
saveInterface <- getShinyOption("save.interface", default = NULL)
saveInterface <- getShinyOption("save.interface")
if (is.null(saveInterface)) {
if (inShinyServer()) {
@@ -296,7 +296,7 @@ RestoreContext <- R6Class("RestoreContext",
# Look for a load.interface function. This will be defined by the hosting
# environment if it supports bookmarking.
loadInterface <- getShinyOption("load.interface", default = NULL)
loadInterface <- getShinyOption("load.interface")
if (is.null(loadInterface)) {
if (inShinyServer()) {

View File

@@ -440,7 +440,7 @@ verticalLayout <- function(..., fluid = TRUE) {
flowLayout <- function(..., cellArgs = list()) {
children <- list(...)
childIdx <- !nzchar(names(children) %||% character(length(children)))
childIdx <- !nzchar(names(children) %OR% character(length(children)))
attribs <- children[!childIdx]
children <- children[childIdx]
@@ -523,7 +523,7 @@ inputPanel <- function(...) {
splitLayout <- function(..., cellWidths = NULL, cellArgs = list()) {
children <- list(...)
childIdx <- !nzchar(names(children) %||% character(length(children)))
childIdx <- !nzchar(names(children) %OR% character(length(children)))
attribs <- children[!childIdx]
children <- children[childIdx]
count <- length(children)

View File

@@ -18,7 +18,7 @@ NULL
#' Bootstrap 3.
#' @param theme One of the following:
#' * `NULL` (the default), which implies a "stock" build of Bootstrap 3.
#' * A [bslib::bs_theme()] object. This can be used to replace a stock
#' * A [bootstraplib::bs_theme()] object. This can be used to replace a stock
#' build of Bootstrap 3 with a customized version of Bootstrap 3 or higher.
#' * A character string pointing to an alternative Bootstrap stylesheet
#' (normally a css file within the www directory, e.g. `www/bootstrap.css`).
@@ -78,7 +78,7 @@ getLang <- function(ui) {
#' @export
bootstrapLib <- function(theme = NULL) {
tagFunction(function() {
# If we're not compiling Bootstrap Sass (from bslib), return the
# If we're not compiling Bootstrap Sass (from bootstraplib), return the
# static Bootstrap build.
if (!is_bs_theme(theme)) {
# We'll enter here if `theme` is the path to a .css file, like that
@@ -100,7 +100,8 @@ bootstrapLib <- function(theme = NULL) {
# option is automatically reset when the app (or session) exits
if (isRunning()) {
setCurrentTheme(theme)
registerThemeDependency(bs_theme_deps)
print("is running! and registering bs_theme_dependencies_css")
registerThemeDependency(bs_theme_dependencies_css)
} else {
# Technically, this a potential issue (someone trying to execute/render
@@ -119,19 +120,22 @@ bootstrapLib <- function(theme = NULL) {
#)
}
bslib::bs_theme_dependencies(theme)
bootstraplib::bs_theme_dependencies(theme)
})
}
# This is defined outside of bootstrapLib() because registerThemeDependency()
# wants a non-anonymous function with a single argument
bs_theme_deps <- function(theme) {
bslib::bs_theme_dependencies(theme)
# wants non-anonymous functions.
bs_theme_dependencies_css <- function(theme) {
deps <- bootstraplib::bs_theme_dependencies(theme)
# Extract out the CSS files only (no need to re-send JS files, even though
# they wouldn't be re-rendered on the client anyway.)
Filter(deps, f = function(dep) !is.null(dep$stylesheet))
}
is_bs_theme <- function(x) {
is_available("bslib", "0.2.0.9000") &&
bslib::is_bs_theme(x)
is_available("bootstraplib", "0.2.0.9000") &&
bootstraplib::is_bs_theme(x)
}
#' Obtain Shiny's Bootstrap Sass theme
@@ -140,14 +144,14 @@ is_bs_theme <- function(x) {
#' styling based on the [bootstrapLib()]'s `theme` value.
#'
#' @return If called at render-time (i.e., inside a [htmltools::tagFunction()]),
#' and [bootstrapLib()]'s `theme` has been set to a [bslib::bs_theme()]
#' and [bootstrapLib()]'s `theme` has been set to a [bootstraplib::bs_theme()]
#' object, then this returns the `theme`. Otherwise, this returns `NULL`.
#' @seealso [getCurrentOutputInfo()], [bootstrapLib()], [htmltools::tagFunction()]
#'
#' @keywords internal
#' @export
getCurrentTheme <- function() {
getShinyOption("bootstrapTheme", default = NULL)
getShinyOption("bootstrapTheme")
}
setCurrentTheme <- function(theme) {
@@ -185,13 +189,14 @@ registerThemeDependency <- function(func) {
# Note that this will automatically scope to the app or session level,
# depending on if this is called from within a session or not.
funcs <- getShinyOption("themeDependencyFuncs", default = list())
funcs <- getShinyOption("themeDependencyFuncs", list())
str(funcs)
# Don't add func if it's already present.
have_func <- any(vapply(funcs, identical, logical(1), func))
if (!have_func) {
funcs[[length(funcs) + 1]] <- func
}
str(funcs)
shinyOptions("themeDependencyFuncs" = funcs)
}
@@ -209,7 +214,7 @@ bootstrapDependency <- function(theme) {
"accessibility/js/bootstrap-accessibility.min.js"
),
stylesheet = c(
theme %||% "css/bootstrap.min.css",
theme %OR% "css/bootstrap.min.css",
# Safely adding accessibility plugin for screen readers and keyboard users; no break for sighted aspects (see https://github.com/paypal/bootstrap-accessibility-plugin)
"accessibility/css/bootstrap-accessibility.css"
),
@@ -868,7 +873,7 @@ findAndMarkSelectedTab <- function(tabs, selected, foundSelected) {
foundSelected <<- TRUE
div <- markTabAsSelected(div)
} else {
tabValue <- div$attribs$`data-value` %||% div$attribs$title
tabValue <- div$attribs$`data-value` %OR% div$attribs$title
if (identical(selected, tabValue)) {
foundSelected <<- TRUE
div <- markTabAsSelected(div)
@@ -1531,7 +1536,6 @@ downloadLink <- function(outputId, label="Download", class=NULL, ...) {
#' [usage examples](http://fontawesome.io/examples/) for details on
#' supported styles).
#' @param lib Icon library to use ("font-awesome" or "glyphicon")
#' @param ... Arguments passed to the `<i>` tag of [htmltools::tags]
#'
#' @return An icon element
#'
@@ -1550,7 +1554,7 @@ downloadLink <- function(outputId, label="Download", class=NULL, ...) {
#' tabPanel("Table", icon = icon("table"))
#' )
#' @export
icon <- function(name, class = NULL, lib = "font-awesome", ...) {
icon <- function(name, class = NULL, lib = "font-awesome") {
prefixes <- list(
"font-awesome" = "fa",
"glyphicon" = "glyphicon"
@@ -1576,7 +1580,7 @@ icon <- function(name, class = NULL, lib = "font-awesome", ...) {
if (!is.null(class))
iconClass <- paste(iconClass, class)
iconTag <- tags$i(class = iconClass, role = "presentation", `aria-label` = paste(name, "icon"), ...)
iconTag <- tags$i(class = iconClass, role = "presentation", `aria-label` = paste(name, "icon"))
# font-awesome needs an additional dependency (glyphicon is in bootstrap)
if (lib == "font-awesome") {

567
R/cache-disk.R Normal file
View File

@@ -0,0 +1,567 @@
#' Create a disk cache object
#'
#' A disk cache object is a key-value store that saves the values as files in a
#' directory on disk. Objects can be stored and retrieved using the `get()`
#' and `set()` methods. Objects are automatically pruned from the cache
#' according to the parameters `max_size`, `max_age`, `max_n`,
#' and `evict`.
#'
#'
#' @section Missing Keys:
#'
#' The `missing` and `exec_missing` parameters controls what happens
#' when `get()` is called with a key that is not in the cache (a cache
#' miss). The default behavior is to return a [key_missing()]
#' object. This is a *sentinel value* that indicates that the key was not
#' present in the cache. You can test if the returned value represents a
#' missing key by using the [is.key_missing()] function. You can
#' also have `get()` return a different sentinel value, like `NULL`.
#' If you want to throw an error on a cache miss, you can do so by providing a
#' function for `missing` that takes one argument, the key, and also use
#' `exec_missing=TRUE`.
#'
#' When the cache is created, you can supply a value for `missing`, which
#' sets the default value to be returned for missing values. It can also be
#' overridden when `get()` is called, by supplying a `missing`
#' argument. For example, if you use `cache$get("mykey", missing =
#' NULL)`, it will return `NULL` if the key is not in the cache.
#'
#' If your cache is configured so that `get()` returns a sentinel value
#' to represent a cache miss, then `set` will also not allow you to store
#' the sentinel value in the cache. It will throw an error if you attempt to
#' do so.
#'
#' Instead of returning the same sentinel value each time there is cache miss,
#' the cache can execute a function each time `get()` encounters missing
#' key. If the function returns a value, then `get()` will in turn return
#' that value. However, a more common use is for the function to throw an
#' error. If an error is thrown, then `get()` will not return a value.
#'
#' To do this, pass a one-argument function to `missing`, and use
#' `exec_missing=TRUE`. For example, if you want to throw an error that
#' prints the missing key, you could do this:
#'
#' \preformatted{
#' diskCache(
#' missing = function(key) {
#' stop("Attempted to get missing key: ", key)
#' },
#' exec_missing = TRUE
#' )
#' }
#'
#' If you use this, the code that calls `get()` should be wrapped with
#' [tryCatch()] to gracefully handle missing keys.
#'
#' @section Cache pruning:
#'
#' Cache pruning occurs when `set()` is called, or it can be invoked
#' manually by calling `prune()`.
#'
#' The disk cache will throttle the pruning so that it does not happen on
#' every call to `set()`, because the filesystem operations for checking
#' the status of files can be slow. Instead, it will prune once in every 20
#' calls to `set()`, or if at least 5 seconds have elapsed since the last
#' prune occurred, whichever is first. These parameters are currently not
#' customizable, but may be in the future.
#'
#' When a pruning occurs, if there are any objects that are older than
#' `max_age`, they will be removed.
#'
#' The `max_size` and `max_n` parameters are applied to the cache as
#' a whole, in contrast to `max_age`, which is applied to each object
#' individually.
#'
#' If the number of objects in the cache exceeds `max_n`, then objects
#' will be removed from the cache according to the eviction policy, which is
#' set with the `evict` parameter. Objects will be removed so that the
#' number of items is `max_n`.
#'
#' If the size of the objects in the cache exceeds `max_size`, then
#' objects will be removed from the cache. Objects will be removed from the
#' cache so that the total size remains under `max_size`. Note that the
#' size is calculated using the size of the files, not the size of disk space
#' used by the files --- these two values can differ because of files are
#' stored in blocks on disk. For example, if the block size is 4096 bytes,
#' then a file that is one byte in size will take 4096 bytes on disk.
#'
#' Another time that objects can be removed from the cache is when
#' `get()` is called. If the target object is older than `max_age`,
#' it will be removed and the cache will report it as a missing value.
#'
#' @section Eviction policies:
#'
#' If `max_n` or `max_size` are used, then objects will be removed
#' from the cache according to an eviction policy. The available eviction
#' policies are:
#'
#' \describe{
#' \item{`"lru"`}{
#' Least Recently Used. The least recently used objects will be removed.
#' This uses the filesystem's mtime property. When "lru" is used, each
#' `get()` is called, it will update the file's mtime.
#' }
#' \item{`"fifo"`}{
#' First-in-first-out. The oldest objects will be removed.
#' }
#' }
#'
#' Both of these policies use files' mtime. Note that some filesystems (notably
#' FAT) have poor mtime resolution. (atime is not used because support for
#' atime is worse than mtime.)
#'
#'
#' @section Sharing among multiple processes:
#'
#' The directory for a DiskCache can be shared among multiple R processes. To
#' do this, each R process should have a DiskCache object that uses the same
#' directory. Each DiskCache will do pruning independently of the others, so if
#' they have different pruning parameters, then one DiskCache may remove cached
#' objects before another DiskCache would do so.
#'
#' Even though it is possible for multiple processes to share a DiskCache
#' directory, this should not be done on networked file systems, because of
#' slow performance of networked file systems can cause problems. If you need
#' a high-performance shared cache, you can use one built on a database like
#' Redis, SQLite, mySQL, or similar.
#'
#' When multiple processes share a cache directory, there are some potential
#' race conditions. For example, if your code calls `exists(key)` to check
#' if an object is in the cache, and then call `get(key)`, the object may
#' be removed from the cache in between those two calls, and `get(key)`
#' will throw an error. Instead of calling the two functions, it is better to
#' simply call `get(key)`, and check that the returned object is not a
#' `key_missing()` object, using `is.key_missing()`. This effectively tests for
#' existence and gets the object in one operation.
#'
#' It is also possible for one processes to prune objects at the same time that
#' another processes is trying to prune objects. If this happens, you may see
#' a warning from `file.remove()` failing to remove a file that has
#' already been deleted.
#'
#'
#' @section Methods:
#'
#' A disk cache object has the following methods:
#'
#' \describe{
#' \item{`get(key, missing, exec_missing)`}{
#' Returns the value associated with `key`. If the key is not in the
#' cache, then it returns the value specified by `missing` or,
#' `missing` is a function and `exec_missing=TRUE`, then
#' executes `missing`. The function can throw an error or return the
#' value. If either of these parameters are specified here, then they
#' will override the defaults that were set when the DiskCache object was
#' created. See section Missing Keys for more information.
#' }
#' \item{`set(key, value)`}{
#' Stores the `key`-`value` pair in the cache.
#' }
#' \item{`exists(key)`}{
#' Returns `TRUE` if the cache contains the key, otherwise
#' `FALSE`.
#' }
#' \item{`size()`}{
#' Returns the number of items currently in the cache.
#' }
#' \item{`keys()`}{
#' Returns a character vector of all keys currently in the cache.
#' }
#' \item{`reset()`}{
#' Clears all objects from the cache.
#' }
#' \item{`destroy()`}{
#' Clears all objects in the cache, and removes the cache directory from
#' disk.
#' }
#' \item{`prune()`}{
#' Prunes the cache, using the parameters specified by `max_size`,
#' `max_age`, `max_n`, and `evict`.
#' }
#' }
#'
#' @param dir Directory to store files for the cache. If `NULL` (the
#' default) it will create and use a temporary directory.
#' @param max_age Maximum age of files in cache before they are evicted, in
#' seconds. Use `Inf` for no age limit.
#' @param max_size Maximum size of the cache, in bytes. If the cache exceeds
#' this size, cached objects will be removed according to the value of the
#' `evict`. Use `Inf` for no size limit.
#' @param max_n Maximum number of objects in the cache. If the number of objects
#' exceeds this value, then cached objects will be removed according to the
#' value of `evict`. Use `Inf` for no limit of number of items.
#' @param evict The eviction policy to use to decide which objects are removed
#' when a cache pruning occurs. Currently, `"lru"` and `"fifo"` are
#' supported.
#' @param destroy_on_finalize If `TRUE`, then when the DiskCache object is
#' garbage collected, the cache directory and all objects inside of it will be
#' deleted from disk. If `FALSE` (the default), it will do nothing when
#' finalized.
#' @param missing A value to return or a function to execute when
#' `get(key)` is called but the key is not present in the cache. The
#' default is a [key_missing()] object. If it is a function to
#' execute, the function must take one argument (the key), and you must also
#' use `exec_missing = TRUE`. If it is a function, it is useful in most
#' cases for it to throw an error, although another option is to return a
#' value. If a value is returned, that value will in turn be returned by
#' `get()`. See section Missing keys for more information.
#' @param exec_missing If `FALSE` (the default), then treat `missing`
#' as a value to return when `get()` results in a cache miss. If
#' `TRUE`, treat `missing` as a function to execute when
#' `get()` results in a cache miss.
#' @param logfile An optional filename or connection object to where logging
#' information will be written. To log to the console, use `stdout()`.
#'
#' @export
diskCache <- function(
dir = NULL,
max_size = 10 * 1024 ^ 2,
max_age = Inf,
max_n = Inf,
evict = c("lru", "fifo"),
destroy_on_finalize = FALSE,
missing = key_missing(),
exec_missing = FALSE,
logfile = NULL)
{
DiskCache$new(dir, max_size, max_age, max_n, evict, destroy_on_finalize,
missing, exec_missing, logfile)
}
DiskCache <- R6Class("DiskCache",
public = list(
initialize = function(
dir = NULL,
max_size = 10 * 1024 ^ 2,
max_age = Inf,
max_n = Inf,
evict = c("lru", "fifo"),
destroy_on_finalize = FALSE,
missing = key_missing(),
exec_missing = FALSE,
logfile = NULL)
{
if (exec_missing && (!is.function(missing) || length(formals(missing)) == 0)) {
stop("When `exec_missing` is true, `missing` must be a function that takes one argument.")
}
if (is.null(dir)) {
dir <- tempfile("DiskCache-")
}
if (!is.numeric(max_size)) stop("max_size must be a number. Use `Inf` for no limit.")
if (!is.numeric(max_age)) stop("max_age must be a number. Use `Inf` for no limit.")
if (!is.numeric(max_n)) stop("max_n must be a number. Use `Inf` for no limit.")
if (!dirExists(dir)) {
private$log(paste0("initialize: Creating ", dir))
dir.create(dir, recursive = TRUE)
}
private$dir <- normalizePath(dir)
private$max_size <- max_size
private$max_age <- max_age
private$max_n <- max_n
private$evict <- match.arg(evict)
private$destroy_on_finalize <- destroy_on_finalize
private$missing <- missing
private$exec_missing <- exec_missing
private$logfile <- logfile
private$prune_last_time <- as.numeric(Sys.time())
},
get = function(key, missing = private$missing, exec_missing = private$exec_missing) {
private$log(paste0('get: key "', key, '"'))
self$is_destroyed(throw = TRUE)
validate_key(key)
private$maybe_prune_single(key)
filename <- private$key_to_filename(key)
# Instead of calling exists() before fetching the value, just try to
# fetch the value. This reduces the risk of a race condition when
# multiple processes share a cache.
read_error <- FALSE
tryCatch(
{
value <- suppressWarnings(readRDS(filename))
if (private$evict == "lru"){
Sys.setFileTime(filename, Sys.time())
}
},
error = function(e) {
read_error <<- TRUE
}
)
if (read_error) {
private$log(paste0('get: key "', key, '" is missing'))
if (exec_missing) {
if (!is.function(missing) || length(formals(missing)) == 0) {
stop("When `exec_missing` is true, `missing` must be a function that takes one argument.")
}
return(missing(key))
} else {
return(missing)
}
}
private$log(paste0('get: key "', key, '" found'))
value
},
set = function(key, value) {
private$log(paste0('set: key "', key, '"'))
self$is_destroyed(throw = TRUE)
validate_key(key)
file <- private$key_to_filename(key)
temp_file <- paste0(file, "-temp-", createUniqueId(8))
save_error <- FALSE
ref_object <- FALSE
tryCatch(
{
saveRDS(value, file = temp_file,
refhook = function(x) {
ref_object <<- TRUE
NULL
}
)
file.rename(temp_file, file)
},
error = function(e) {
save_error <<- TRUE
# Unlike file.remove(), unlink() does not raise warning if file does
# not exist.
unlink(temp_file)
}
)
if (save_error) {
private$log(paste0('set: key "', key, '" error'))
stop('Error setting value for key "', key, '".')
}
if (ref_object) {
private$log(paste0('set: value is a reference object'))
warning("A reference object was cached in a serialized format. The restored object may not work as expected.")
}
private$prune_throttled()
invisible(self)
},
exists = function(key) {
self$is_destroyed(throw = TRUE)
validate_key(key)
file.exists(private$key_to_filename(key))
},
# Return all keys in the cache
keys = function() {
self$is_destroyed(throw = TRUE)
files <- dir(private$dir, "\\.rds$")
sub("\\.rds$", "", files)
},
remove = function(key) {
private$log(paste0('remove: key "', key, '"'))
self$is_destroyed(throw = TRUE)
validate_key(key)
file.remove(private$key_to_filename(key))
invisible(self)
},
reset = function() {
private$log(paste0('reset'))
self$is_destroyed(throw = TRUE)
file.remove(dir(private$dir, "\\.rds$", full.names = TRUE))
invisible(self)
},
prune = function() {
# TODO: It would be good to add parameters `n` and `size`, so that the
# cache can be pruned to `max_n - n` and `max_size - size` before adding
# an object. Right now we prune after adding the object, so the cache
# can temporarily grow past the limits. The reason we don't do this now
# is because it is expensive to find the size of the serialized object
# before adding it.
private$log('prune')
self$is_destroyed(throw = TRUE)
current_time <- Sys.time()
filenames <- dir(private$dir, "\\.rds$", full.names = TRUE)
info <- file.info(filenames)
info <- info[info$isdir == FALSE, ]
info$name <- rownames(info)
rownames(info) <- NULL
# Files could be removed between the dir() and file.info() calls. The
# entire row for such files will have NA values. Remove those rows.
info <- info[!is.na(info$size), ]
# 1. Remove any files where the age exceeds max age.
if (is.finite(private$max_age)) {
timediff <- as.numeric(current_time - info$mtime, units = "secs")
rm_idx <- timediff > private$max_age
if (any(rm_idx)) {
private$log(paste0("prune max_age: Removing ", paste(info$name[rm_idx], collapse = ", ")))
rm_success <- file.remove(info$name[rm_idx])
# This maps rm_success back into the TRUEs in the rm_idx vector.
# If (for example) rm_idx is c(F,T,F,T,T) and rm_success is c(T,F,T),
# then this line modifies rm_idx to be c(F,T,F,F,T).
rm_idx[rm_idx] <- rm_success
info <- info[!rm_idx, ]
}
}
# Sort objects by priority. The sorting is done in a function which can be
# called multiple times but only does the work the first time.
info_is_sorted <- FALSE
ensure_info_is_sorted <- function() {
if (info_is_sorted) return()
info <<- info[order(info$mtime, decreasing = TRUE), ]
info_is_sorted <<- TRUE
}
# 2. Remove files if there are too many.
if (is.finite(private$max_n) && nrow(info) > private$max_n) {
ensure_info_is_sorted()
rm_idx <- seq_len(nrow(info)) > private$max_n
private$log(paste0("prune max_n: Removing ", paste(info$name[rm_idx], collapse = ", ")))
rm_success <- file.remove(info$name[rm_idx])
rm_idx[rm_idx] <- rm_success
info <- info[!rm_idx, ]
}
# 3. Remove files if cache is too large.
if (is.finite(private$max_size) && sum(info$size) > private$max_size) {
ensure_info_is_sorted()
cum_size <- cumsum(info$size)
rm_idx <- cum_size > private$max_size
private$log(paste0("prune max_size: Removing ", paste(info$name[rm_idx], collapse = ", ")))
rm_success <- file.remove(info$name[rm_idx])
rm_idx[rm_idx] <- rm_success
info <- info[!rm_idx, ]
}
private$prune_last_time <- as.numeric(current_time)
invisible(self)
},
size = function() {
self$is_destroyed(throw = TRUE)
length(dir(private$dir, "\\.rds$"))
},
destroy = function() {
if (self$is_destroyed()) {
return(invisible(self))
}
private$log(paste0("destroy: Removing ", private$dir))
# First create a sentinel file so that other processes sharing this
# cache know that the cache is to be destroyed. This is needed because
# the recursive unlink is not atomic: another process can add a file to
# the directory after unlink starts removing files but before it removes
# the directory, and when that happens, the directory removal will fail.
file.create(file.path(private$dir, "__destroyed__"))
# Remove all the .rds files. This will not remove the setinel file.
file.remove(dir(private$dir, "\\.rds$", full.names = TRUE))
# Next remove dir recursively, including sentinel file.
unlink(private$dir, recursive = TRUE)
private$destroyed <- TRUE
invisible(self)
},
is_destroyed = function(throw = FALSE) {
if (!dirExists(private$dir) ||
file.exists(file.path(private$dir, "__destroyed__")))
{
# It's possible for another process to destroy a shared cache directory
private$destroyed <- TRUE
}
if (throw) {
if (private$destroyed) {
stop("Attempted to use cache which has been destroyed:\n ", private$dir)
}
} else {
private$destroyed
}
},
finalize = function() {
if (private$destroy_on_finalize) {
self$destroy()
}
}
),
private = list(
dir = NULL,
max_age = NULL,
max_size = NULL,
max_n = NULL,
evict = NULL,
destroy_on_finalize = NULL,
destroyed = FALSE,
missing = NULL,
exec_missing = FALSE,
logfile = NULL,
prune_throttle_counter = 0,
prune_last_time = NULL,
key_to_filename = function(key) {
validate_key(key)
# Additional validation. This 80-char limit is arbitrary, and is
# intended to avoid hitting a filename length limit on Windows.
if (nchar(key) > 80) {
stop("Invalid key: key must have fewer than 80 characters.")
}
file.path(private$dir, paste0(key, ".rds"))
},
# A wrapper for prune() that throttles it, because prune() can be
# expensive due to filesystem operations. This function will prune only
# once every 20 times it is called, or if it has been more than 5 seconds
# since the last time the cache was actually pruned, whichever is first.
# In the future, the behavior may be customizable.
prune_throttled = function() {
# Count the number of times prune() has been called.
private$prune_throttle_counter <- private$prune_throttle_counter + 1
if (private$prune_throttle_counter > 20 ||
private$prune_last_time - as.numeric(Sys.time()) > 5)
{
self$prune()
private$prune_throttle_counter <- 0
}
},
# Prunes a single object if it exceeds max_age. If the object does not
# exceed max_age, or if the object doesn't exist, do nothing.
maybe_prune_single = function(key) {
obj <- private$cache[[key]]
if (is.null(obj)) return()
timediff <- as.numeric(Sys.time()) - obj$mtime
if (timediff > private$max_age) {
private$log(paste0("pruning single object exceeding max_age: Removing ", key))
rm(list = key, envir = private$cache)
}
},
log = function(text) {
if (is.null(private$logfile)) return()
text <- paste0(format(Sys.time(), "[%Y-%m-%d %H:%M:%OS3] DiskCache "), text)
cat(text, sep = "\n", file = private$logfile, append = TRUE)
}
)
)

365
R/cache-memory.R Normal file
View File

@@ -0,0 +1,365 @@
#' Create a memory cache object
#'
#' A memory cache object is a key-value store that saves the values in an
#' environment. Objects can be stored and retrieved using the `get()` and
#' `set()` methods. Objects are automatically pruned from the cache
#' according to the parameters `max_size`, `max_age`, `max_n`,
#' and `evict`.
#'
#' In a `MemoryCache`, R objects are stored directly in the cache; they are
#' not *not* serialized before being stored in the cache. This contrasts
#' with other cache types, like [diskCache()], where objects are
#' serialized, and the serialized object is cached. This can result in some
#' differences of behavior. For example, as long as an object is stored in a
#' MemoryCache, it will not be garbage collected.
#'
#'
#' @section Missing keys:
#' The `missing` and `exec_missing` parameters controls what happens
#' when `get()` is called with a key that is not in the cache (a cache
#' miss). The default behavior is to return a [key_missing()]
#' object. This is a *sentinel value* that indicates that the key was not
#' present in the cache. You can test if the returned value represents a
#' missing key by using the [is.key_missing()] function. You can
#' also have `get()` return a different sentinel value, like `NULL`.
#' If you want to throw an error on a cache miss, you can do so by providing a
#' function for `missing` that takes one argument, the key, and also use
#' `exec_missing=TRUE`.
#'
#' When the cache is created, you can supply a value for `missing`, which
#' sets the default value to be returned for missing values. It can also be
#' overridden when `get()` is called, by supplying a `missing`
#' argument. For example, if you use `cache$get("mykey", missing =
#' NULL)`, it will return `NULL` if the key is not in the cache.
#'
#' If your cache is configured so that `get()` returns a sentinel value
#' to represent a cache miss, then `set` will also not allow you to store
#' the sentinel value in the cache. It will throw an error if you attempt to
#' do so.
#'
#' Instead of returning the same sentinel value each time there is cache miss,
#' the cache can execute a function each time `get()` encounters missing
#' key. If the function returns a value, then `get()` will in turn return
#' that value. However, a more common use is for the function to throw an
#' error. If an error is thrown, then `get()` will not return a value.
#'
#' To do this, pass a one-argument function to `missing`, and use
#' `exec_missing=TRUE`. For example, if you want to throw an error that
#' prints the missing key, you could do this:
#'
#' \preformatted{
#' diskCache(
#' missing = function(key) {
#' stop("Attempted to get missing key: ", key)
#' },
#' exec_missing = TRUE
#' )
#' }
#'
#' If you use this, the code that calls `get()` should be wrapped with
#' [tryCatch()] to gracefully handle missing keys.
#'
#' @section Cache pruning:
#'
#' Cache pruning occurs when `set()` is called, or it can be invoked
#' manually by calling `prune()`.
#'
#' When a pruning occurs, if there are any objects that are older than
#' `max_age`, they will be removed.
#'
#' The `max_size` and `max_n` parameters are applied to the cache as
#' a whole, in contrast to `max_age`, which is applied to each object
#' individually.
#'
#' If the number of objects in the cache exceeds `max_n`, then objects
#' will be removed from the cache according to the eviction policy, which is
#' set with the `evict` parameter. Objects will be removed so that the
#' number of items is `max_n`.
#'
#' If the size of the objects in the cache exceeds `max_size`, then
#' objects will be removed from the cache. Objects will be removed from the
#' cache so that the total size remains under `max_size`. Note that the
#' size is calculated using the size of the files, not the size of disk space
#' used by the files --- these two values can differ because of files are
#' stored in blocks on disk. For example, if the block size is 4096 bytes,
#' then a file that is one byte in size will take 4096 bytes on disk.
#'
#' Another time that objects can be removed from the cache is when
#' `get()` is called. If the target object is older than `max_age`,
#' it will be removed and the cache will report it as a missing value.
#'
#' @section Eviction policies:
#'
#' If `max_n` or `max_size` are used, then objects will be removed
#' from the cache according to an eviction policy. The available eviction
#' policies are:
#'
#' \describe{
#' \item{`"lru"`}{
#' Least Recently Used. The least recently used objects will be removed.
#' This uses the filesystem's atime property. Some filesystems do not
#' support atime, or have a very low atime resolution. The DiskCache will
#' check for atime support, and if the filesystem does not support atime,
#' a warning will be issued and the "fifo" policy will be used instead.
#' }
#' \item{`"fifo"`}{
#' First-in-first-out. The oldest objects will be removed.
#' }
#' }
#'
#' @section Methods:
#'
#' A disk cache object has the following methods:
#'
#' \describe{
#' \item{`get(key, missing, exec_missing)`}{
#' Returns the value associated with `key`. If the key is not in the
#' cache, then it returns the value specified by `missing` or,
#' `missing` is a function and `exec_missing=TRUE`, then
#' executes `missing`. The function can throw an error or return the
#' value. If either of these parameters are specified here, then they
#' will override the defaults that were set when the DiskCache object was
#' created. See section Missing Keys for more information.
#' }
#' \item{`set(key, value)`}{
#' Stores the `key`-`value` pair in the cache.
#' }
#' \item{`exists(key)`}{
#' Returns `TRUE` if the cache contains the key, otherwise
#' `FALSE`.
#' }
#' \item{`size()`}{
#' Returns the number of items currently in the cache.
#' }
#' \item{`keys()`}{
#' Returns a character vector of all keys currently in the cache.
#' }
#' \item{`reset()`}{
#' Clears all objects from the cache.
#' }
#' \item{`destroy()`}{
#' Clears all objects in the cache, and removes the cache directory from
#' disk.
#' }
#' \item{`prune()`}{
#' Prunes the cache, using the parameters specified by `max_size`,
#' `max_age`, `max_n`, and `evict`.
#' }
#' }
#'
#' @inheritParams diskCache
#'
#' @export
memoryCache <- function(
max_size = 10 * 1024 ^ 2,
max_age = Inf,
max_n = Inf,
evict = c("lru", "fifo"),
missing = key_missing(),
exec_missing = FALSE,
logfile = NULL)
{
MemoryCache$new(max_size, max_age, max_n, evict, missing, exec_missing, logfile)
}
MemoryCache <- R6Class("MemoryCache",
public = list(
initialize = function(
max_size = 10 * 1024 ^ 2,
max_age = Inf,
max_n = Inf,
evict = c("lru", "fifo"),
missing = key_missing(),
exec_missing = FALSE,
logfile = NULL)
{
if (exec_missing && (!is.function(missing) || length(formals(missing)) == 0)) {
stop("When `exec_missing` is true, `missing` must be a function that takes one argument.")
}
if (!is.numeric(max_size)) stop("max_size must be a number. Use `Inf` for no limit.")
if (!is.numeric(max_age)) stop("max_age must be a number. Use `Inf` for no limit.")
if (!is.numeric(max_n)) stop("max_n must be a number. Use `Inf` for no limit.")
private$cache <- fastmap()
private$max_size <- max_size
private$max_age <- max_age
private$max_n <- max_n
private$evict <- match.arg(evict)
private$missing <- missing
private$exec_missing <- exec_missing
private$logfile <- logfile
},
get = function(key, missing = private$missing, exec_missing = private$exec_missing) {
private$log(paste0('get: key "', key, '"'))
validate_key(key)
private$maybe_prune_single(key)
if (!self$exists(key)) {
private$log(paste0('get: key "', key, '" is missing'))
if (exec_missing) {
if (!is.function(missing) || length(formals(missing)) == 0) {
stop("When `exec_missing` is true, `missing` must be a function that takes one argument.")
}
return(missing(key))
} else {
return(missing)
}
}
private$log(paste0('get: key "', key, '" found'))
value <- private$cache$get(key)$value
value
},
set = function(key, value) {
private$log(paste0('set: key "', key, '"'))
validate_key(key)
time <- as.numeric(Sys.time())
# Only record size if we're actually using max_size for pruning.
if (is.finite(private$max_size)) {
# Reported size is rough! See ?object.size.
size <- as.numeric(object.size(value))
} else {
size <- NULL
}
private$cache$set(key, list(
key = key,
value = value,
size = size,
mtime = time,
atime = time
))
self$prune()
invisible(self)
},
exists = function(key) {
validate_key(key)
private$cache$has(key)
},
keys = function() {
private$cache$keys()
},
remove = function(key) {
private$log(paste0('remove: key "', key, '"'))
validate_key(key)
private$cache$remove(key)
invisible(self)
},
reset = function() {
private$log(paste0('reset'))
private$cache$reset()
invisible(self)
},
prune = function() {
private$log(paste0('prune'))
info <- private$object_info()
# 1. Remove any objects where the age exceeds max age.
if (is.finite(private$max_age)) {
time <- as.numeric(Sys.time())
timediff <- time - info$mtime
rm_idx <- timediff > private$max_age
if (any(rm_idx)) {
private$log(paste0("prune max_age: Removing ", paste(info$key[rm_idx], collapse = ", ")))
private$cache$remove(info$key[rm_idx])
info <- info[!rm_idx, ]
}
}
# Sort objects by priority, according to eviction policy. The sorting is
# done in a function which can be called multiple times but only does
# the work the first time.
info_is_sorted <- FALSE
ensure_info_is_sorted <- function() {
if (info_is_sorted) return()
if (private$evict == "lru") {
info <<- info[order(info$atime, decreasing = TRUE), ]
} else if (private$evict == "fifo") {
info <<- info[order(info$mtime, decreasing = TRUE), ]
} else {
stop('Unknown eviction policy "', private$evict, '"')
}
info_is_sorted <<- TRUE
}
# 2. Remove objects if there are too many.
if (is.finite(private$max_n) && nrow(info) > private$max_n) {
ensure_info_is_sorted()
rm_idx <- seq_len(nrow(info)) > private$max_n
private$log(paste0("prune max_n: Removing ", paste(info$key[rm_idx], collapse = ", ")))
private$cache$remove(info$key[rm_idx])
info <- info[!rm_idx, ]
}
# 3. Remove objects if cache is too large.
if (is.finite(private$max_size) && sum(info$size) > private$max_size) {
ensure_info_is_sorted()
cum_size <- cumsum(info$size)
rm_idx <- cum_size > private$max_size
private$log(paste0("prune max_size: Removing ", paste(info$key[rm_idx], collapse = ", ")))
private$cache$remove(info$key[rm_idx])
info <- info[!rm_idx, ]
}
invisible(self)
},
size = function() {
length(self$keys())
}
),
private = list(
cache = NULL,
max_age = NULL,
max_size = NULL,
max_n = NULL,
evict = NULL,
missing = NULL,
exec_missing = NULL,
logfile = NULL,
# Prunes a single object if it exceeds max_age. If the object does not
# exceed max_age, or if the object doesn't exist, do nothing.
maybe_prune_single = function(key) {
if (!is.finite(private$max_age)) return()
obj <- private$cache$get(key)
if (is.null(obj)) return()
timediff <- as.numeric(Sys.time()) - obj$mtime
if (timediff > private$max_age) {
private$log(paste0("pruning single object exceeding max_age: Removing ", key))
private$cache$remove(key)
}
},
object_info = function() {
keys <- private$cache$keys()
data.frame(
key = keys,
size = vapply(keys, function(key) private$cache$get(key)$size, 0),
mtime = vapply(keys, function(key) private$cache$get(key)$mtime, 0),
atime = vapply(keys, function(key) private$cache$get(key)$atime, 0),
stringsAsFactors = FALSE
)
},
log = function(text) {
if (is.null(private$logfile)) return()
text <- paste0(format(Sys.time(), "[%Y-%m-%d %H:%M:%OS3] MemoryCache "), text)
cat(text, sep = "\n", file = private$logfile, append = TRUE)
}
)
)

View File

@@ -1,25 +1,9 @@
# For our purposes, cache objects must support these methods.
is_cache_object <- function(x) {
# Use tryCatch in case the object does not support `$`.
tryCatch(
is.function(x$get) && is.function(x$set),
error = function(e) FALSE
)
}
# Given a cache object, or string "app" or "session", return appropriate cache
# object.
resolve_cache_object <- function(cache, session) {
if (identical(cache, "app")) {
cache <- getShinyOption("cache", default = NULL)
} else if (identical(cache, "session")) {
cache <- session$cache
validate_key <- function(key) {
if (!is.character(key) || length(key) != 1 || nchar(key) == 0) {
stop("Invalid key: key must be single non-empty string.")
}
if (is_cache_object(cache)) {
return(cache)
if (grepl("[^a-z0-9]", key)) {
stop("Invalid key: ", key, ". Only lowercase letters and numbers are allowed.")
}
stop('`cache` must either be "app", "session", or a cache object with methods, `$get`, and `$set`.')
}

View File

@@ -1,102 +0,0 @@
#' Print message for deprecated functions in Shiny
#'
#' To disable these messages, use `options(shiny.deprecation.messages=FALSE)`.
#'
#' @param new Name of replacement function.
#' @param msg Message to print. If used, this will override the default message.
#' @param old Name of deprecated function.
#' @param version The last version of Shiny before the item was deprecated.
#' @keywords internal
shinyDeprecated <- function(new=NULL, msg=NULL,
old=as.character(sys.call(sys.parent()))[1L],
version = NULL) {
if (getOption("shiny.deprecation.messages") %||% TRUE == FALSE)
return(invisible())
if (is.null(msg)) {
msg <- paste(old, "is deprecated.")
if (!is.null(new)) {
msg <- paste(msg, "Please use", new, "instead.",
"To disable this message, run options(shiny.deprecation.messages=FALSE)")
}
}
if (!is.null(version)) {
msg <- paste0(msg, " (Last used in version ", version, ")")
}
# Similar to .Deprecated(), but print a message instead of warning
message(msg)
}
deprecatedEnvQuotedMessage <- function(env_arg = "env", quoted_arg = "quoted") {
# Enable this message in a future version of Shiny, perhaps in a dev_edition()
# mode.
# shinyDeprecated(msg = paste(
# sprintf("The `%s` and `%s` arguments are deprecated.", env_arg, quoted_arg),
# "Please use quosures from rlang instead.",
# "See https://github.com/rstudio/shiny/issues/3108 for more information."
# ))
}
#' Create disk cache (deprecated)
#'
#' @param exec_missing Deprecated.
#' @inheritParams cachem::cache_disk
#' @keywords internal
#' @export
diskCache <- function(
dir = NULL,
max_size = 500 * 1024 ^ 2,
max_age = Inf,
max_n = Inf,
evict = c("lru", "fifo"),
destroy_on_finalize = FALSE,
missing = key_missing(),
exec_missing = FALSE,
logfile = NULL)
{
shinyDeprecated("cachem::cache_disk", version = "1.5.1")
cachem::cache_disk(
dir = dir,
max_size = max_size,
max_age = max_age,
max_n = max_n,
evict = evict,
destroy_on_finalize = destroy_on_finalize,
missing = missing,
logfile = logfile
)
}
#' Create memory cache (deprecated)
#'
#' @param exec_missing Deprecated.
#' @inheritParams cachem::cache_mem
#' @keywords internal
#' @export
memoryCache <- function(
max_size = 200 * 1024 ^ 2,
max_age = Inf,
max_n = Inf,
evict = c("lru", "fifo"),
missing = key_missing(),
exec_missing = FALSE,
logfile = NULL)
{
shinyDeprecated("cachem::cache_mem", version = "1.5.1")
cachem::cache_mem(
max_size = max_size,
max_age = max_age,
max_n = max_n,
evict = evict,
missing = missing,
logfile = logfile
)
}

View File

@@ -10,7 +10,7 @@ check_suggested <- function(package, version = NULL) {
msg <- paste0(
sQuote(package),
if (is.na(version %||% NA)) "" else paste0("(>= ", version, ")"),
if (is.na(version %OR% NA)) "" else paste0("(>= ", version, ")"),
" must be installed for this functionality."
)

View File

@@ -92,21 +92,11 @@ brushedPoints <- function(df, brush, xvar = NULL, yvar = NULL,
use_x <- grepl("x", brush$direction)
use_y <- grepl("y", brush$direction)
# We transitioned to using %||% in Shiny 1.6.0. Previously, these vars could
# be NA, because the old %OR% operator recognized NA. These warnings and
# the NULL replacement are here just to ease the transition in case anyone is
# using NA. We can remove these checks in a future version of Shiny.
# https://github.com/rstudio/shiny/pull/3172
if (is_na(xvar)) { xvar <- NULL; warning("xvar should be NULL, not NA.") }
if (is_na(yvar)) { yvar <- NULL; warning("yvar should be NULL, not NA.") }
if (is_na(panelvar1)) { panelvar1 <- NULL; warning("panelvar1 should be NULL, not NA.") }
if (is_na(panelvar2)) { panelvar2 <- NULL; warning("panelvar2 should be NULL, not NA.") }
# Try to extract vars from brush object
xvar <- xvar %||% brush$mapping$x
yvar <- yvar %||% brush$mapping$y
panelvar1 <- panelvar1 %||% brush$mapping$panelvar1
panelvar2 <- panelvar2 %||% brush$mapping$panelvar2
xvar <- xvar %OR% brush$mapping$x
yvar <- yvar %OR% brush$mapping$y
panelvar1 <- panelvar1 %OR% brush$mapping$panelvar1
panelvar2 <- panelvar2 %OR% brush$mapping$panelvar2
# Filter out x and y values
keep_rows <- rep(TRUE, nrow(df))
@@ -240,21 +230,11 @@ nearPoints <- function(df, coordinfo, xvar = NULL, yvar = NULL,
stop("nearPoints requires a click/hover/double-click object with x and y values.")
}
# We transitioned to using %||% in Shiny 1.6.0. Previously, these vars could
# be NA, because the old %OR% operator recognized NA. These warnings and
# the NULL replacement are here just to ease the transition in case anyone is
# using NA. We can remove these checks in a future version of Shiny.
# https://github.com/rstudio/shiny/pull/3172
if (is_na(xvar)) { xvar <- NULL; warning("xvar should be NULL, not NA.") }
if (is_na(yvar)) { yvar <- NULL; warning("yvar should be NULL, not NA.") }
if (is_na(panelvar1)) { panelvar1 <- NULL; warning("panelvar1 should be NULL, not NA.") }
if (is_na(panelvar2)) { panelvar2 <- NULL; warning("panelvar2 should be NULL, not NA.") }
# Try to extract vars from coordinfo object
xvar <- xvar %||% coordinfo$mapping$x
yvar <- yvar %||% coordinfo$mapping$y
panelvar1 <- panelvar1 %||% coordinfo$mapping$panelvar1
panelvar2 <- panelvar2 %||% coordinfo$mapping$panelvar2
xvar <- xvar %OR% coordinfo$mapping$x
yvar <- yvar %OR% coordinfo$mapping$y
panelvar1 <- panelvar1 %OR% coordinfo$mapping$panelvar1
panelvar2 <- panelvar2 %OR% coordinfo$mapping$panelvar2
if (is.null(xvar))
stop("nearPoints: not able to automatically infer `xvar` from coordinfo")

View File

@@ -4,12 +4,12 @@ startPNG <- function(filename, width, height, res, ...) {
# to use ragg (say, instead of showtext, for custom font rendering).
# In the next shiny release, this option will likely be superseded in
# favor of a fully customizable graphics device option
if ((getOption('shiny.useragg') %||% FALSE) && is_available("ragg")) {
if ((getOption('shiny.useragg') %OR% FALSE) && is_available("ragg")) {
pngfun <- ragg::agg_png
} else if (capabilities("aqua")) {
# i.e., png(type = 'quartz')
pngfun <- grDevices::png
} else if ((getOption('shiny.usecairo') %||% TRUE) && is_available("Cairo")) {
} else if ((getOption('shiny.usecairo') %OR% TRUE) && is_available("Cairo")) {
pngfun <- Cairo::CairoPNG
} else {
# i.e., png(type = 'cairo')
@@ -24,7 +24,7 @@ startPNG <- function(filename, width, height, res, ...) {
# to plot-time, but it shouldn't hurt to inform other the device directly as well
if (is.null(args$bg) && isNamespaceLoaded("thematic")) {
# TODO: use :: once thematic is on CRAN
args$bg <- utils::getFromNamespace("thematic_get_option", "thematic")("bg", "white", resolve = FALSE)
args$bg <- utils::getFromNamespace("thematic_get_option", "thematic")("bg", "white")
# auto vals aren't resolved until plot time, so if we see one, resolve it
if (isTRUE("auto" == args$bg)) {
args$bg <- getCurrentOutputInfo()[["bg"]]()

View File

@@ -149,7 +149,7 @@ datePickerDependency <- function(theme) {
})();
</script>"
),
bslib::bs_dependency_defer(datePickerCSS)
bootstraplib::bs_dependency_defer(datePickerCSS)
)
}
@@ -165,11 +165,11 @@ datePickerCSS <- function(theme) {
scss_file <- system.file(package = "shiny", "www/shared/datepicker/scss/build3.scss")
bslib::bs_dependency(
bootstraplib::bs_dependency(
input = sass::sass_file(scss_file),
theme = theme,
name = "bootstrap-datepicker",
version = datePickerVersion,
cache_key_extra = shinyPackageVersion()
cache_key_extra = utils::packageVersion("shiny")
)
}

View File

@@ -233,54 +233,48 @@ selectizeIt <- function(inputId, select, options, nonempty = FALSE) {
}
selectizeDependency <- function() {
bslib::bs_dependency_defer(selectizeDependencyFunc)
selectizeVersion <- "0.12.4"
selectizeDependency <- function(theme) {
list(
htmlDependency(
"selectize-js",
selectizeVersion,
src = c(href = "shared/selectize"),
script = c(
"js/selectize.min.js",
# Accessibility plugin for screen readers (https://github.com/SLMNBJ/selectize-plugin-a11y):
"accessibility/js/selectize-plugin-a11y.min.js"
)
),
bootstraplib::bs_dependency_defer(selectizeCSS)
)
}
selectizeDependencyFunc <- function(theme) {
selectizeVersion <- "0.12.4"
selectizeCSS <- function(theme) {
if (!is_bs_theme(theme)) {
return(selectizeStaticDependency(selectizeVersion))
return(htmlDependency(
name = "selectize-css",
version = selectizeVersion,
src = c(href = "shared/selectize"),
stylesheet = "css/selectize.bootstrap3.css"
))
}
selectizeDir <- system.file(package = "shiny", "www/shared/selectize/")
stylesheet <- file.path(
selectizeDir, "scss",
if ("3" %in% bslib::theme_version(theme)) {
scss_file <- system.file(
package = "shiny", "www/shared/selectize/scss",
if ("3" %in% bootstraplib::theme_version(theme)) {
"selectize.bootstrap3.scss"
} else {
"selectize.bootstrap4.scss"
}
)
# It'd be cleaner to ship the JS in a separate, href-based,
# HTML dependency (which we currently do for other themable widgets),
# but DT, crosstalk, and maybe other pkgs include selectize JS/CSS
# in HTML dependency named selectize, so if we were to change that
# name, the JS/CSS would be loaded/included twice, which leads to
# strange issues, especially since we now include a 3rd party
# accessibility plugin https://github.com/rstudio/shiny/pull/3153
script <- file.path(
selectizeDir, c("js/selectize.min.js", "accessibility/js/selectize-plugin-a11y.min.js")
)
bslib::bs_dependency(
input = sass::sass_file(stylesheet),
bootstraplib::bs_dependency(
input = sass::sass_file(scss_file),
theme = theme,
name = "selectize",
version = selectizeVersion,
cache_key_extra = shinyPackageVersion(),
.dep_args = list(script = script)
)
}
selectizeStaticDependency <- function(version) {
htmlDependency(
"selectize", version,
src = c(href = "shared/selectize"),
stylesheet = "css/selectize.bootstrap3.css",
script = c(
"js/selectize.min.js",
"accessibility/js/selectize-plugin-a11y.min.js"
)
cache_key_extra = utils::packageVersion("shiny")
)
}

View File

@@ -224,7 +224,7 @@ ionRangeSliderDependency <- function() {
src = c(href = "shared/strftime"),
script = "strftime-min.js"
),
bslib::bs_dependency_defer(ionRangeSliderDependencyCSS)
bootstraplib::bs_dependency_defer(ionRangeSliderDependencyCSS)
)
}
@@ -241,13 +241,8 @@ ionRangeSliderDependencyCSS <- function(theme) {
# Remap some variable names for ionRangeSlider's scss
sass_input <- list(
list(
# The bootswatch materia theme sets $input-bg: transparent;
# which is an issue for the slider's handle(s) (#3130)
bg = "if(alpha($input-bg)==0, $body-bg, $input-bg)",
fg = sprintf(
"if(alpha($input-color)==0, $%s, $input-color)",
if ("3" %in% bslib::theme_version(theme)) "text-color" else "body-color"
),
bg = "$input-bg",
fg = "$input-color",
accent = "$component-active-bg",
`font-family` = "$font-family-base"
),
@@ -256,12 +251,12 @@ ionRangeSliderDependencyCSS <- function(theme) {
)
)
bslib::bs_dependency(
bootstraplib::bs_dependency(
input = sass_input,
theme = theme,
name = "ionRangeSlider",
version = ionRangeSliderVersion,
cache_key_extra = shinyPackageVersion()
cache_key_extra = utils::packageVersion("shiny")
)
}

View File

@@ -25,7 +25,7 @@ shiny_rmd_warning <- function() {
#' @rdname knitr_methods
knit_print.shiny.appobj <- function(x, ...) {
opts <- x$options %||% list()
opts <- x$options %OR% list()
width <- if (is.null(opts$width)) "100%" else opts$width
height <- if (is.null(opts$height)) "400" else opts$height

15
R/map.R
View File

@@ -1,3 +1,18 @@
# TESTS
# Simple set/get
# Simple remove
# Simple containsKey
# Simple keys
# Simple values
# Simple clear
# Get of unknown key returns NULL
# Remove of unknown key does nothing
# Setting a key twice always results in last-one-wins
# /TESTS
# Note that Map objects can't be saved in one R session and restored in
# another, because they are based on fastmap, which uses an external pointer,
# and external pointers can't be saved and restored in another session.
#' @importFrom fastmap fastmap
Map <- R6Class(
'Map',

View File

@@ -309,7 +309,7 @@ HandlerManager <- R6Class("HandlerManager",
createHttpuvApp = function() {
list(
onHeaders = function(req) {
maxSize <- getOption('shiny.maxRequestSize') %||% (5 * 1024 * 1024)
maxSize <- getOption('shiny.maxRequestSize') %OR% (5 * 1024 * 1024)
if (maxSize <= 0)
return(NULL)
@@ -346,7 +346,7 @@ HandlerManager <- R6Class("HandlerManager",
),
catch = function(err) {
httpResponse(status = 500L,
content_type = "text/html; charset=UTF-8",
content_type = "text/html",
content = as.character(htmltools::htmlTemplate(
system.file("template", "error.html", package = "shiny"),
message = conditionMessage(err)

View File

@@ -156,7 +156,6 @@ makeExtraMethods <- function() {
"sendInsertUI",
"sendModal",
"setCurrentTheme",
"getCurrentTheme",
"sendNotification",
"sendProgress",
"sendRemoveTab",
@@ -235,9 +234,9 @@ MockShinySession <- R6Class(
progressStack = 'Stack',
#' @field token On a real `ShinySession`, used to identify this instance in URLs.
token = 'character',
#' @field cache The session cache object.
#' @field cache The session cache MemoryCache.
cache = NULL,
#' @field appcache The app cache object.
#' @field appcache The app cache MemoryCache.
appcache = NULL,
#' @field restoreContext Part of bookmarking support in a real
#' `ShinySession` but always `NULL` for a `MockShinySession`.
@@ -278,8 +277,8 @@ MockShinySession <- R6Class(
# Copy app-level options
self$options <- getCurrentAppState()$options
self$cache <- cachem::cache_mem()
self$appcache <- cachem::cache_mem()
self$cache <- MemoryCache$new()
self$appcache <- MemoryCache$new()
# Adds various generated noop and error-producing method implementations.
# Note that noop methods can be configured to produce warnings by setting

View File

@@ -31,36 +31,11 @@ createSessionProxy <- function(parentSession, ...) {
# but not `session$userData <- TRUE`) from within a module
# without any hacks (see PR #1732)
if (identical(x[[name]], value)) return(x)
# Special case for $options (issue #3112)
if (name == "options") {
session <- find_ancestor_session(x)
session[[name]] <- value
return(x)
}
stop("Attempted to assign value on session proxy.")
}
`[[<-.session_proxy` <- `$<-.session_proxy`
# Given a session_proxy, search `parent` recursively to find the real
# ShinySession object. If given a ShinySession, simply return it.
find_ancestor_session <- function(x, depth = 20) {
if (depth < 0) {
stop("ShinySession not found")
}
if (inherits(x, "ShinySession")) {
return(x)
}
if (inherits(x, "session_proxy")) {
return(find_ancestor_session(.subset2(x, "parent"), depth-1))
}
stop("ShinySession not found")
}
#' Shiny modules
#'
#' Shiny's module feature lets you break complicated UI and server logic into

View File

@@ -947,7 +947,6 @@ Observable <- R6Class(
#' @param domain See [domains].
#' @param ..stacktraceon Advanced use only. For stack manipulation purposes; see
#' [stacktrace()].
#' @param ... Not used.
#' @return a function, wrapped in a S3 class "reactive"
#'
#' @examples
@@ -969,30 +968,20 @@ Observable <- R6Class(
#' isolate(reactiveC())
#' isolate(reactiveD())
#' @export
reactive <- function(x, env = parent.frame(), quoted = FALSE,
...,
label = NULL,
domain = getDefaultReactiveDomain(),
..stacktraceon = TRUE)
{
check_dots_empty()
x <- get_quosure(x, env, quoted)
fun <- as_function(x)
# as_function returns a function that takes `...`. We need one that takes no
# args.
formals(fun) <- list()
reactive <- function(x, env = parent.frame(), quoted = FALSE, label = NULL,
domain = getDefaultReactiveDomain(),
..stacktraceon = TRUE) {
fun <- exprToFunction(x, env, quoted)
# Attach a label and a reference to the original user source for debugging
label <- exprToLabel(get_expr(x), "reactive", label)
srcref <- attr(substitute(x), "srcref", exact = TRUE)
if (is.null(label)) {
label <- rexprSrcrefToLabel(srcref[[1]],
sprintf('reactive(%s)', paste(deparse(body(fun)), collapse='\n')))
}
if (length(srcref) >= 2) attr(label, "srcref") <- srcref[[2]]
attr(label, "srcfile") <- srcFileOfRef(srcref[[1]])
o <- Observable$new(fun, label, domain, ..stacktraceon = ..stacktraceon)
structure(
o$getValue,
observable = o,
cacheHint = list(userExpr = zap_srcref(get_expr(x))),
class = c("reactiveExpr", "reactive", "function")
)
structure(o$getValue, observable = o, class = c("reactiveExpr", "reactive", "function"))
}
# Given the srcref to a reactive expression, attempts to figure out what the
@@ -1064,14 +1053,6 @@ execCount <- function(x) {
stop('Unexpected argument to execCount')
}
# Internal utility functions for extracting things out of reactives.
reactive_get_value_func <- function(x) {
attr(x, "observable", exact = TRUE)$.origFunc
}
reactive_get_domain <- function(x) {
attr(x, "observable", exact = TRUE)$.domain
}
# Observer ------------------------------------------------------------------
Observer <- R6Class(
@@ -1332,8 +1313,6 @@ Observer <- R6Class(
#' automatically destroyed when its domain (if any) ends.
#' @param ..stacktraceon Advanced use only. For stack manipulation purposes; see
#' [stacktrace()].
#' @param ... Not used.
#'
#' @return An observer reference class object. This object has the following
#' methods:
#' \describe{
@@ -1388,36 +1367,18 @@ Observer <- R6Class(
#' # are at the console, you can force a flush with flushReact()
#' shiny:::flushReact()
#' @export
observe <- function(x, env = parent.frame(), quoted = FALSE,
...,
label = NULL,
suspended = FALSE,
priority = 0,
domain = getDefaultReactiveDomain(),
autoDestroy = TRUE,
..stacktraceon = TRUE)
{
check_dots_empty()
observe <- function(x, env=parent.frame(), quoted=FALSE, label=NULL,
suspended=FALSE, priority=0,
domain=getDefaultReactiveDomain(), autoDestroy = TRUE,
..stacktraceon = TRUE) {
x <- get_quosure(x, env, quoted)
fun <- as_function(x)
# as_function returns a function that takes `...`. We need one that takes no
# args.
formals(fun) <- list()
fun <- exprToFunction(x, env, quoted)
if (is.null(label))
label <- sprintf('observe(%s)', paste(deparse(body(fun)), collapse='\n'))
if (is.null(label)) {
label <- sprintf('observe(%s)', paste(deparse(get_expr(x)), collapse='\n'))
}
o <- Observer$new(
fun,
label = label,
suspended = suspended,
priority = priority,
domain = domain,
autoDestroy = autoDestroy,
..stacktraceon = ..stacktraceon
)
o <- Observer$new(fun, label=label, suspended=suspended, priority=priority,
domain=domain, autoDestroy=autoDestroy,
..stacktraceon=..stacktraceon)
invisible(o)
}
@@ -1810,7 +1771,6 @@ reactivePoll <- function(intervalMillis, session, checkFunc, valueFunc) {
rv <- reactiveValues(cookie = isolate(checkFunc()))
re_finalized <- FALSE
env <- environment()
o <- observe({
# When no one holds a reference to the reactive returned from
@@ -1818,7 +1778,7 @@ reactivePoll <- function(intervalMillis, session, checkFunc, valueFunc) {
# firing and hold onto resources.
if (re_finalized) {
o$destroy()
rm(o, envir = env)
rm(o, envir = parent.env(environment()))
return()
}
@@ -2161,7 +2121,6 @@ maskReactiveContext <- function(expr) {
#' after the first time that the code in `handlerExpr` is run. This
#' pattern is useful when you want to subscribe to a event that should only
#' happen once.
#' @param ... Currently not used.
#'
#' @return `observeEvent` returns an observer reference class object (see
#' [observe()]). `eventReactive` returns a reactive expression
@@ -2234,38 +2193,42 @@ maskReactiveContext <- function(expr) {
observeEvent <- function(eventExpr, handlerExpr,
event.env = parent.frame(), event.quoted = FALSE,
handler.env = parent.frame(), handler.quoted = FALSE,
...,
label = NULL, suspended = FALSE, priority = 0,
domain = getDefaultReactiveDomain(), autoDestroy = TRUE,
ignoreNULL = TRUE, ignoreInit = FALSE, once = FALSE)
{
check_dots_empty()
ignoreNULL = TRUE, ignoreInit = FALSE, once = FALSE) {
eventExpr <- get_quosure(eventExpr, event.env, event.quoted)
handlerExpr <- get_quosure(handlerExpr, handler.env, handler.quoted)
eventFunc <- exprToFunction(eventExpr, event.env, event.quoted)
if (is.null(label))
label <- sprintf('observeEvent(%s)', paste(deparse(body(eventFunc)), collapse='\n'))
eventFunc <- wrapFunctionLabel(eventFunc, "observeEventExpr", ..stacktraceon = TRUE)
if (is.null(label)) {
label <- sprintf('observeEvent(%s)', paste(deparse(get_expr(eventExpr)), collapse='\n'))
}
handlerFunc <- exprToFunction(handlerExpr, handler.env, handler.quoted)
handlerFunc <- wrapFunctionLabel(handlerFunc, "observeEventHandler", ..stacktraceon = TRUE)
handler <- inject(observe(
!!handlerExpr,
label = label,
suspended = suspended,
priority = priority,
domain = domain,
autoDestroy = TRUE,
..stacktraceon = FALSE # TODO: Does this go in the bindEvent?
))
initialized <- FALSE
o <- inject(bindEvent(
ignoreNULL = ignoreNULL,
ignoreInit = ignoreInit,
once = once,
label = label,
!!eventExpr,
x = handler
))
o <- observe({
hybrid_chain(
{eventFunc()},
function(value) {
if (ignoreInit && !initialized) {
initialized <<- TRUE
return()
}
if (ignoreNULL && isNullEvent(value)) {
return()
}
if (once) {
on.exit(o$destroy())
}
isolate(handlerFunc())
}
)
}, label = label, suspended = suspended, priority = priority, domain = domain,
autoDestroy = TRUE, ..stacktraceon = FALSE)
invisible(o)
}
@@ -2275,26 +2238,34 @@ observeEvent <- function(eventExpr, handlerExpr,
eventReactive <- function(eventExpr, valueExpr,
event.env = parent.frame(), event.quoted = FALSE,
value.env = parent.frame(), value.quoted = FALSE,
...,
label = NULL, domain = getDefaultReactiveDomain(),
ignoreNULL = TRUE, ignoreInit = FALSE)
{
check_dots_empty()
ignoreNULL = TRUE, ignoreInit = FALSE) {
eventExpr <- get_quosure(eventExpr, event.env, event.quoted)
valueExpr <- get_quosure(valueExpr, value.env, value.quoted)
eventFunc <- exprToFunction(eventExpr, event.env, event.quoted)
if (is.null(label))
label <- sprintf('eventReactive(%s)', paste(deparse(body(eventFunc)), collapse='\n'))
eventFunc <- wrapFunctionLabel(eventFunc, "eventReactiveExpr", ..stacktraceon = TRUE)
if (is.null(label)) {
label <- sprintf('eventReactive(%s)', paste(deparse(get_expr(eventExpr)), collapse='\n'))
}
handlerFunc <- exprToFunction(valueExpr, value.env, value.quoted)
handlerFunc <- wrapFunctionLabel(handlerFunc, "eventReactiveHandler", ..stacktraceon = TRUE)
invisible(inject(bindEvent(
ignoreNULL = ignoreNULL,
ignoreInit = ignoreInit,
label = label,
!!eventExpr,
x = reactive(!!valueExpr, domain = domain, label = label)
)))
initialized <- FALSE
invisible(reactive({
hybrid_chain(
eventFunc(),
function(value) {
if (ignoreInit && !initialized) {
initialized <<- TRUE
req(FALSE)
}
req(!ignoreNULL || !isNullEvent(value))
isolate(handlerFunc())
}
)
}, label = label, domain = domain, ..stacktraceon = FALSE))
}
isNullEvent <- function(value) {
@@ -2454,7 +2425,7 @@ debounce <- function(r, millis, priority = 100, domain = getDefaultReactiveDomai
now <- getDomainTimeMs(domain)
if (now >= v$when) {
# Mod by 999999999 to get predictable overflow behavior
v$trigger <- isolate(v$trigger %||% 0) %% 999999999 + 1
v$trigger <- isolate(v$trigger %OR% 0) %% 999999999 + 1
v$when <- NULL
} else {
invalidateLater(v$when - now)

View File

@@ -1,7 +1,6 @@
#' Plot output with cached images
#'
#' Renders a reactive plot, with plot images cached to disk. As of Shiny 1.6.0,
#' this is a shortcut for using [bindCache()] with [renderPlot()].
#' Renders a reactive plot, with plot images cached to disk.
#'
#' `expr` is an expression that generates a plot, similar to that in
#' `renderPlot`. Unlike with `renderPlot`, this expression does not
@@ -41,6 +40,95 @@
#' if there are multiple plots that have the same `cacheKeyExpr`, they
#' will not have cache key collisions.
#'
#' @section Cache scoping:
#'
#' There are a number of different ways you may want to scope the cache. For
#' example, you may want each user session to have their own plot cache, or
#' you may want each run of the application to have a cache (shared among
#' possibly multiple simultaneous user sessions), or you may want to have a
#' cache that persists even after the application is shut down and started
#' again.
#'
#' To control the scope of the cache, use the `cache` parameter. There
#' are two ways of having Shiny automatically create and clean up the disk
#' cache.
#'
#' \describe{
#' \item{1}{To scope the cache to one run of a Shiny application (shared
#' among possibly multiple user sessions), use `cache="app"`. This
#' is the default. The cache will be shared across multiple sessions, so
#' there is potentially a large performance benefit if there are many users
#' of the application. When the application stops running, the cache will
#' be deleted. If plots cannot be safely shared across users, this should
#' not be used.}
#' \item{2}{To scope the cache to one session, use `cache="session"`.
#' When a new user session starts --- in other words, when a web browser
#' visits the Shiny application --- a new cache will be created on disk
#' for that session. When the session ends, the cache will be deleted.
#' The cache will not be shared across multiple sessions.}
#' }
#'
#' If either `"app"` or `"session"` is used, the cache will be 10 MB
#' in size, and will be stored stored in memory, using a
#' [memoryCache()] object. Note that the cache space will be shared
#' among all cached plots within a single application or session.
#'
#' In some cases, you may want more control over the caching behavior. For
#' example, you may want to use a larger or smaller cache, share a cache
#' among multiple R processes, or you may want the cache to persist across
#' multiple runs of an application, or even across multiple R processes.
#'
#' To use different settings for an application-scoped cache, you can call
#' [shinyOptions()] at the top of your app.R, server.R, or
#' global.R. For example, this will create a cache with 20 MB of space
#' instead of the default 10 MB:
#' \preformatted{
#' shinyOptions(cache = memoryCache(size = 20e6))
#' }
#'
#' To use different settings for a session-scoped cache, you can call
#' [shinyOptions()] at the top of your server function. To use
#' the session-scoped cache, you must also call `renderCachedPlot` with
#' `cache="session"`. This will create a 20 MB cache for the session:
#' \preformatted{
#' function(input, output, session) {
#' shinyOptions(cache = memoryCache(size = 20e6))
#'
#' output$plot <- renderCachedPlot(
#' ...,
#' cache = "session"
#' )
#' }
#' }
#'
#' If you want to create a cache that is shared across multiple concurrent
#' R processes, you can use a [diskCache()]. You can create an
#' application-level shared cache by putting this at the top of your app.R,
#' server.R, or global.R:
#' \preformatted{
#' shinyOptions(cache = diskCache(file.path(dirname(tempdir()), "myapp-cache"))
#' }
#'
#' This will create a subdirectory in your system temp directory named
#' `myapp-cache` (replace `myapp-cache` with a unique name of
#' your choosing). On most platforms, this directory will be removed when
#' your system reboots. This cache will persist across multiple starts and
#' stops of the R process, as long as you do not reboot.
#'
#' To have the cache persist even across multiple reboots, you can create the
#' cache in a location outside of the temp directory. For example, it could
#' be a subdirectory of the application:
#' \preformatted{
#' shinyOptions(cache = diskCache("./myapp-cache"))
#' }
#'
#' In this case, resetting the cache will have to be done manually, by deleting
#' the directory.
#'
#' You can also scope a cache to just one plot, or selected plots. To do that,
#' create a [memoryCache()] or [diskCache()], and pass it
#' as the `cache` argument of `renderCachedPlot`.
#'
#' @section Interactive plots:
#'
#' `renderCachedPlot` can be used to create interactive plots. See
@@ -48,7 +136,6 @@
#'
#'
#' @inheritParams renderPlot
#' @inheritParams bindCache
#' @param cacheKeyExpr An expression that returns a cache key. This key should
#' be a unique identifier for a plot: the assumption is that if the cache key
#' is the same, then the plot will be the same.
@@ -59,13 +146,16 @@
#' possible pixel dimension. See [sizeGrowthRatio()] for more
#' information on the default sizing policy.
#' @param res The resolution of the PNG, in pixels per inch.
#' @param cache The scope of the cache, or a cache object. This can be
#' `"app"` (the default), `"session"`, or a cache object like
#' a [diskCache()]. See the Cache Scoping section for more
#' information.
#' @param width,height not used. They are specified via the argument
#' `sizePolicy`.
#'
#' @seealso See [renderPlot()] for the regular, non-cached version of this
#' function. It can be used with [bindCache()] to get the same effect as
#' `renderCachedPlot()`. For more about configuring caches, see
#' [cachem::cache_mem()] and [cachem::cache_disk()].
#' @seealso See [renderPlot()] for the regular, non-cached version of
#' this function. For more about configuring caches, see
#' [memoryCache()] and [diskCache()].
#'
#'
#' @examples
@@ -156,7 +246,7 @@
#' xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
#' },
#' cacheKeyExpr = { list(input$n) },
#' cache = cachem::cache_mem()
#' cache = memoryCache()
#' )
#' output$plot2 <- renderCachedPlot({
#' Sys.sleep(2) # Add an artificial delay
@@ -165,7 +255,7 @@
#' xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
#' },
#' cacheKeyExpr = { list(input$n) },
#' cache = cachem::cache_mem()
#' cache = memoryCache()
#' )
#' }
#' )
@@ -176,22 +266,22 @@
#' # At the top of app.R, this set the application-scoped cache to be a memory
#' # cache that is 20 MB in size, and where cached objects expire after one
#' # hour.
#' shinyOptions(cache = cachem::cache_mem(max_size = 20e6, max_age = 3600))
#' shinyOptions(cache = memoryCache(max_size = 20e6, max_age = 3600))
#'
#' # At the top of app.R, this set the application-scoped cache to be a disk
#' # cache that can be shared among multiple concurrent R processes, and is
#' # deleted when the system reboots.
#' shinyOptions(cache = cachem::cache_disk(file.path(dirname(tempdir()), "myapp-cache"))
#' shinyOptions(cache = diskCache(file.path(dirname(tempdir()), "myapp-cache"))
#'
#' # At the top of app.R, this set the application-scoped cache to be a disk
#' # cache that can be shared among multiple concurrent R processes, and
#' # persists on disk across reboots.
#' shinyOptions(cache = cachem::cache_disk("./myapp-cache"))
#' shinyOptions(cache = diskCache("./myapp-cache"))
#'
#' # At the top of the server function, this set the session-scoped cache to be
#' # a memory cache that is 5 MB in size.
#' server <- function(input, output, session) {
#' shinyOptions(cache = cachem::cache_mem(max_size = 5e6))
#' shinyOptions(cache = memoryCache(max_size = 5e6))
#'
#' output$plot <- renderCachedPlot(
#' ...,
@@ -213,29 +303,275 @@ renderCachedPlot <- function(expr,
height = NULL
) {
expr <- substitute(expr)
if (!is_quosure(expr)) {
expr <- new_quosure(expr, env = parent.frame())
}
# This ..stacktraceon is matched by a ..stacktraceoff.. when plotFunc
# is called
installExprFunction(expr, "func", parent.frame(), quoted = FALSE, ..stacktraceon = TRUE)
# This is so that the expr doesn't re-execute by itself; it needs to be
# triggered by the cache key (or width/height) changing.
isolatedFunc <- function() isolate(func())
cacheKeyExpr <- substitute(cacheKeyExpr)
if (!is_quosure(cacheKeyExpr)) {
cacheKeyExpr <- new_quosure(cacheKeyExpr, env = parent.frame())
}
args <- list(...)
if (!is.null(width) || !is.null(height)) {
warning("Unused argument(s) 'width' and/or 'height'. ",
"'sizePolicy' is used instead.")
}
inject(
bindCache(
renderPlot(!!expr, res = res, alt = alt, outputArgs = outputArgs, ...),
!!cacheKeyExpr,
sizePolicy = sizePolicy,
cache = cache
cacheKeyExpr <- substitute(cacheKeyExpr)
# The real cache key we'll use also includes width, height, res, pixelratio.
# This is just the part supplied by the user.
userCacheKey <- reactive(cacheKeyExpr, env = parent.frame(), quoted = TRUE, label = "userCacheKey")
ensureCacheSetup <- function() {
# For our purposes, cache objects must support these methods.
isCacheObject <- function(x) {
# Use tryCatch in case the object does not support `$`.
tryCatch(
is.function(x$get) && is.function(x$set),
error = function(e) FALSE
)
}
if (isCacheObject(cache)) {
# If `cache` is already a cache object, do nothing
return()
} else if (identical(cache, "app")) {
cache <<- getShinyOption("cache")
} else if (identical(cache, "session")) {
cache <<- session$cache
} else {
stop('`cache` must either be "app", "session", or a cache object with methods, `$get`, and `$set`.')
}
}
# The width and height of the plot to draw, given from sizePolicy. These
# values get filled by an observer below.
fitDims <- reactiveValues(width = NULL, height = NULL)
# Make sure alt param to be reactive function
if (is.reactive(alt))
altWrapper <- alt
else if (is.function(alt))
altWrapper <- reactive({ alt() })
else
altWrapper <- function() { alt }
resizeObserver <- NULL
ensureResizeObserver <- function() {
if (!is.null(resizeObserver))
return()
# Given the actual width/height of the image in the browser, this gets the
# width/height from sizePolicy() and pushes those values into `fitDims`.
# It's done this way so that the `fitDims` only change (and cause
# invalidations) when the rendered image size changes, and not every time
# the browser's <img> tag changes size.
doResizeCheck <- function() {
width <- session$clientData[[paste0('output_', outputName, '_width')]]
height <- session$clientData[[paste0('output_', outputName, '_height')]]
if (is.null(width)) width <- 0
if (is.null(height)) height <- 0
rect <- sizePolicy(c(width, height))
fitDims$width <- rect[1]
fitDims$height <- rect[2]
}
# Run it once immediately, then set up the observer
isolate(doResizeCheck())
resizeObserver <<- observe(doResizeCheck())
}
# Vars to store session and output, so that they can be accessed from
# the plotObj() reactive.
session <- NULL
outputName <- NULL
drawReactive <- reactive(label = "plotObj", {
hybrid_chain(
# Depend on the user cache key, even though we don't use the value. When
# it changes, it can cause the drawReactive to re-execute. (Though
# drawReactive will not necessarily re-execute --- it must be called from
# renderFunc, which happens only if there's a cache miss.)
userCacheKey(),
function(userCacheKeyValue) {
# Get width/height, but don't depend on them.
isolate({
width <- fitDims$width
height <- fitDims$height
# Make sure alt text to be reactive function
alt <- altWrapper()
})
pixelratio <- session$clientData$pixelratio %OR% 1
do.call("drawPlot", c(
list(
name = outputName,
session = session,
func = isolatedFunc,
width = width,
height = height,
alt = alt,
pixelratio = pixelratio,
res = res
),
args
))
},
catch = function(reason) {
# Non-isolating read. A common reason for errors in plotting is because
# the dimensions are too small. By taking a dependency on width/height,
# we can try again if the plot output element changes size.
fitDims$width
fitDims$height
# Propagate the error
stop(reason)
}
)
)
})
# This function is the one that's returned from renderPlot(), and gets
# wrapped in an observer when the output value is assigned.
renderFunc <- function(shinysession, name, ...) {
outputName <<- name
session <<- shinysession
ensureCacheSetup()
ensureResizeObserver()
hybrid_chain(
# This use of the userCacheKey() sets up the reactive dependency that
# causes plot re-draw events. These may involve pulling from the cache,
# replaying a display list, or re-executing user code.
userCacheKey(),
function(userCacheKeyResult) {
width <- fitDims$width
height <- fitDims$height
alt <- altWrapper()
pixelratio <- session$clientData$pixelratio %OR% 1
key <- digest::digest(list(outputName, userCacheKeyResult, width, height, res, pixelratio), "xxhash64")
plotObj <- cache$get(key)
# First look in cache.
# Case 1. cache hit.
if (!is.key_missing(plotObj)) {
return(list(
cacheHit = TRUE,
key = key,
plotObj = plotObj,
width = width,
height = height,
alt = alt,
pixelratio = pixelratio
))
}
# If not in cache, hybrid_chain call to drawReactive
#
# Two more possible cases:
# 2. drawReactive will re-execute and return a plot that's the
# correct size.
# 3. It will not re-execute, but it will return the previous value,
# which is the wrong size. It will include a valid display list
# which can be used by resizeSavedPlot.
hybrid_chain(
drawReactive(),
function(drawReactiveResult) {
# Pass along the key for caching in the next stage
list(
cacheHit = FALSE,
key = key,
plotObj = drawReactiveResult,
width = width,
height = height,
alt = alt,
pixelratio = pixelratio
)
}
)
},
function(possiblyAsyncResult) {
hybrid_chain(possiblyAsyncResult, function(result) {
width <- result$width
height <- result$height
alt <- result$alt
pixelratio <- result$pixelratio
# Three possibilities when we get here:
# 1. There was a cache hit. No need to set a value in the cache.
# 2. There was a cache miss, and the plotObj is already the correct
# size (because drawReactive re-executed). In this case, we need
# to cache it.
# 3. There was a cache miss, and the plotObj was not the corect size.
# In this case, we need to replay the display list, and then cache
# the result.
if (!result$cacheHit) {
# If the image is already the correct size, this just returns the
# object unchanged.
result$plotObj <- do.call("resizeSavedPlot", c(
list(
name,
shinysession,
result$plotObj,
width,
height,
alt,
pixelratio,
res
),
args
))
# Save a cached copy of the plotObj. The recorded displaylist for
# the plot can't be serialized and restored properly within the same
# R session, so we NULL it out before saving. (The image data and
# other metadata be saved and restored just fine.) Displaylists can
# also be very large (~1.5MB for a basic ggplot), and they would not
# be commonly used. Note that displaylist serialization was fixed in
# revision 74506 (2e6c669), and should be in R 3.6. A MemoryCache
# doesn't need to serialize objects, so it could actually save a
# display list, but for the reasons listed previously, it's
# generally not worth it.
# The plotResult is not the same as the recordedPlot (it is used to
# retrieve coordmap information for ggplot2 objects) but it is only
# used in conjunction with the recordedPlot, and we'll remove it
# because it can be quite large.
result$plotObj$plotResult <- NULL
result$plotObj$recordedPlot <- NULL
cache$set(result$key, result$plotObj)
}
img <- result$plotObj$img
# Replace exact pixel dimensions; instead, the max-height and
# max-width will be set to 100% from CSS.
img$class <- "shiny-scalable"
img$width <- NULL
img$height <- NULL
img
})
}
)
}
# If renderPlot isn't going to adapt to the height of the div, then the
# div needs to adapt to the height of renderPlot. By default, plotOutput
# sets the height to 400px, so to make it adapt we need to override it
# with NULL.
outputFunc <- plotOutput
formals(outputFunc)['height'] <- list(NULL)
markRenderFunction(outputFunc, renderFunc, outputArgs = outputArgs)
}

View File

@@ -62,11 +62,9 @@ renderPlot <- function(expr, width = 'auto', height = 'auto', res = 72, ...,
env = parent.frame(), quoted = FALSE,
execOnResize = FALSE, outputArgs = list()
) {
expr <- get_quosure(expr, env, quoted)
# This ..stacktraceon is matched by a ..stacktraceoff.. when plotFunc
# is called
func <- quoToFunction(expr, "renderPlot", ..stacktraceon = TRUE)
installExprFunction(expr, "func", env, quoted, ..stacktraceon = TRUE)
args <- list(...)
@@ -91,9 +89,7 @@ renderPlot <- function(expr, width = 'auto', height = 'auto', res = 72, ...,
else
altWrapper <- function() { alt }
# This is the function that will be used as getDims by default, but it can be
# overridden (which happens when bindCache() is used).
getDimsDefault <- function() {
getDims <- function() {
width <- widthWrapper()
height <- heightWrapper()
@@ -112,7 +108,6 @@ renderPlot <- function(expr, width = 'auto', height = 'auto', res = 72, ...,
# the plotObj() reactive.
session <- NULL
outputName <- NULL
getDims <- NULL
# Calls drawPlot, invoking the user-provided `func` (which may or may not
# return a promise). The idea is that the (cached) return value from this
@@ -123,7 +118,7 @@ renderPlot <- function(expr, width = 'auto', height = 'auto', res = 72, ...,
{
# If !execOnResize, don't invalidate when width/height changes.
dims <- if (execOnResize) getDims() else isolate(getDims())
pixelratio <- session$clientData$pixelratio %||% 1
pixelratio <- session$clientData$pixelratio %OR% 1
do.call("drawPlot", c(
list(
name = outputName,
@@ -150,19 +145,15 @@ renderPlot <- function(expr, width = 'auto', height = 'auto', res = 72, ...,
# This function is the one that's returned from renderPlot(), and gets
# wrapped in an observer when the output value is assigned.
# The `get_dims` parameter defaults to `getDimsDefault`. However, it can be
# overridden, so that `bindCache` can use a different version.
renderFunc <- function(shinysession, name, ..., get_dims = getDimsDefault) {
renderFunc <- function(shinysession, name, ...) {
outputName <<- name
session <<- shinysession
if (is.null(getDims)) getDims <<- get_dims
hybrid_chain(
drawReactive(),
function(result) {
dims <- getDims()
pixelratio <- session$clientData$pixelratio %||% 1
pixelratio <- session$clientData$pixelratio %OR% 1
result <- do.call("resizeSavedPlot", c(
list(name, shinysession, result, dims$width, dims$height, altWrapper(), pixelratio, res),
args
@@ -180,14 +171,7 @@ renderPlot <- function(expr, width = 'auto', height = 'auto', res = 72, ...,
outputFunc <- plotOutput
if (!identical(height, 'auto')) formals(outputFunc)['height'] <- list(NULL)
markedFunc <- markRenderFunction(
outputFunc,
renderFunc,
outputArgs,
cacheHint = list(userExpr = get_expr(expr), res = res)
)
class(markedFunc) <- c("shiny.renderPlot", class(markedFunc))
markedFunc
markRenderFunction(outputFunc, renderFunc, outputArgs = outputArgs)
}
resizeSavedPlot <- function(name, session, result, width, height, alt, pixelratio, res, ...) {
@@ -253,9 +237,8 @@ drawPlot <- function(name, session, func, width, height, alt, pixelratio, res, .
promises::with_promise_domain(domain, {
hybrid_chain(
func(),
function(value) {
res <- withVisible(value)
if (res$visible) {
function(value, .visible) {
if (.visible) {
# A modified version of print.ggplot which returns the built ggplot object
# as well as the gtable grob. This overrides the ggplot::print.ggplot
# method, but only within the context of renderPlot. The reason this needs
@@ -273,7 +256,7 @@ drawPlot <- function(name, session, func, width, height, alt, pixelratio, res, .
# similar to ggplot2. But for base graphics, it would already have
# been rendered when func was called above, and the print should
# have no effect.
result <- ..stacktraceon..(print(res$value))
result <- ..stacktraceon..(print(value))
# TODO jcheng 2017-04-11: Verify above ..stacktraceon..
})
result
@@ -610,10 +593,6 @@ find_panel_info_api <- function(b) {
coord <- ggplot2::summarise_coord(b)
layers <- ggplot2::summarise_layers(b)
`%NA_OR%` <- function(x, y) {
if (is_na(x)) y else x
}
# Given x and y scale objects and a coord object, return a list that has
# the bases of log transformations for x and y, or NULL if it's not a
# log transform.
@@ -630,8 +609,8 @@ find_panel_info_api <- function(b) {
# First look for log base in scale, then coord; otherwise NULL.
list(
x = get_log_base(xscale$trans) %NA_OR% coord$xlog %NA_OR% NULL,
y = get_log_base(yscale$trans) %NA_OR% coord$ylog %NA_OR% NULL
x = get_log_base(xscale$trans) %OR% coord$xlog %OR% NULL,
y = get_log_base(yscale$trans) %OR% coord$ylog %OR% NULL
)
}

View File

@@ -53,10 +53,8 @@ renderTable <- function(expr, striped = FALSE, hover = FALSE,
rownames = FALSE, colnames = TRUE,
digits = NULL, na = "NA", ...,
env = parent.frame(), quoted = FALSE,
outputArgs=list())
{
expr <- get_quosure(expr, env, quoted)
func <- quoToFunction(expr, "renderTable")
outputArgs=list()) {
installExprFunction(expr, "func", env, quoted)
if (!is.function(spacing)) spacing <- match.arg(spacing)

View File

@@ -142,8 +142,8 @@ runApp <- function(appDir=getwd(),
shinyOptions(appToken = createUniqueId(8))
# Set up default cache for app.
if (is.null(getShinyOption("cache", default = NULL))) {
shinyOptions(cache = cachem::cache_mem(max_size = 200 * 1024^2))
if (is.null(getShinyOption("cache"))) {
shinyOptions(cache = MemoryCache$new())
}
# Extract appOptions (which is a list) and store them as shinyOptions, for

View File

@@ -80,7 +80,7 @@ addResourcePath <- function(prefix, directoryPath) {
# If a shiny app is currently running, dynamically register this path with
# the corresponding httpuv server object.
if (!is.null(getShinyOption("server", default = NULL)))
if (!is.null(getShinyOption("server")))
{
getShinyOption("server")$setStaticPath(.list = stats::setNames(normalizedPath, prefix))
}

View File

@@ -165,8 +165,7 @@ getShinyOption <- function(name, default = NULL) {
#' `shinyOptions()`.
#'
#' \describe{ \item{cache}{A caching object that will be used by
#' [renderCachedPlot()]. If not specified, a [cachem::cache_mem()] will be
#' used.} }
#' [renderCachedPlot()]. If not specified, a [memoryCache()] will be used.} }
#'
#' @param ... Options to set, with the form `name = value`.
#' @aliases shiny-options

199
R/shiny.R
View File

@@ -28,15 +28,6 @@ NULL
#' @import methods
NULL
#' @importFrom digest digest
#' @importFrom promises promise promise_resolve promise_reject is.promising
#' as.promise
#' @importFrom rlang quo enquo as_function get_expr get_env new_function enquos
#' eval_tidy expr pairlist2 new_quosure enexpr as_quosure is_quosure inject
#' enquos0 zap_srcref %||% is_na
#' @importFrom ellipsis check_dots_empty check_dots_unnamed
NULL
createUniqueId <- function(bytes, prefix = "", suffix = "") {
withPrivateSeed({
paste(
@@ -278,18 +269,6 @@ workerId <- local({
#' character vector, as in `input=c("x", "y")`. The format can be
#' "rds" or "json".
#' }
#' \item{setCurrentTheme(theme)}{
#' Sets the current [bootstrapLib()] theme, which updates the value of
#' [getCurrentTheme()], invalidates `session$getCurrentTheme()`, and calls
#' function(s) registered with [registerThemeDependency()] with provided
#' `theme`. If those function calls return [htmltools::htmlDependency()]s with
#' `stylesheet`s, then those stylesheets are "refreshed" (i.e., the new
#' stylesheets are inserted on the page and the old ones are disabled and
#' removed).
#' }
#' \item{getCurrentTheme()}{
#' A reactive read of the current [bootstrapLib()] theme.
#' }
#'
#' @name session
NULL
@@ -381,7 +360,6 @@ ShinySession <- R6Class(
currentOutputName = NULL, # Name of the currently-running output
outputInfo = list(), # List of information for each output
testSnapshotUrl = character(0),
currentThemeDependency = NULL, # ReactiveVal for taking dependency on theme
sendResponse = function(requestMsg, value) {
if (is.null(requestMsg$tag)) {
@@ -487,7 +465,7 @@ ShinySession <- R6Class(
# The format of the response that will be sent back. Defaults to
# "json" unless requested otherwise. The only other valid value is
# "rds".
format <- params$format %||% "json"
format <- params$format %OR% "json"
values <- list()
@@ -618,21 +596,21 @@ ShinySession <- R6Class(
# function has been set, return the identity function.
getSnapshotPreprocessOutput = function(name) {
fun <- attr(private$.outputs[[name]], "snapshotPreprocess", exact = TRUE)
fun %||% identity
fun %OR% identity
},
# Get the snapshotPreprocessInput function for an input name. If no preprocess
# function has been set, return the identity function.
getSnapshotPreprocessInput = function(name) {
fun <- private$.input$getMeta(name, "shiny.snapshot.preprocess")
fun %||% identity
fun %OR% identity
},
# See cycleStartAction
startCycle = function() {
# TODO: This should check for busyCount == 0L, and remove the checks from
# the call sites
if (private$busyCount == 0L && length(private$cycleStartActionQueue) > 0) {
if (length(private$cycleStartActionQueue) > 0) {
head <- private$cycleStartActionQueue[[1L]]
private$cycleStartActionQueue <- private$cycleStartActionQueue[-1L]
@@ -653,7 +631,13 @@ ShinySession <- R6Class(
# busyCount, it's possible we're calling startCycle spuriously; that's
# OK, it's essentially a no-op in that case.
on.exit({
later::later(private$startCycle)
if (private$busyCount == 0L && length(private$cycleStartActionQueue) > 0L) {
later::later(function() {
if (private$busyCount == 0L) {
private$startCycle()
}
})
}
}, add = TRUE)
head()
@@ -712,7 +696,7 @@ ShinySession <- R6Class(
# Copy app-level options
self$options <- getCurrentAppState()$options
self$cache <- cachem::cache_mem(max_size = 200 * 1024^2)
self$cache <- MemoryCache$new()
private$bookmarkCallbacks <- Callbacks$new()
private$bookmarkedCallbacks <- Callbacks$new()
@@ -722,8 +706,6 @@ ShinySession <- R6Class(
private$testMode <- getShinyOption("testmode", default = FALSE)
private$enableTestSnapshot()
private$currentThemeDependency <- reactiveVal(0)
private$registerSessionEndCallbacks()
if (!is.null(websocket$request$HTTP_SHINY_SERVER_CREDENTIALS)) {
@@ -1298,11 +1280,6 @@ ShinySession <- R6Class(
)
},
getCurrentTheme = function() {
private$currentThemeDependency()
getShinyOption("bootstrapTheme")
},
setCurrentTheme = function(theme) {
# This function does three things: (1) sets theme as the current
# bootstrapTheme, (2) re-executes any registered theme dependencies, and
@@ -1311,11 +1288,8 @@ ShinySession <- R6Class(
# Note that this will automatically scope to the session.
shinyOptions(bootstrapTheme = theme)
# Invalidate
private$currentThemeDependency(isolate(private$currentThemeDependency()) + 1)
# Call any theme dependency functions and make sure we get a list of deps back
funcs <- getShinyOption("themeDependencyFuncs", default = list())
funcs <- getShinyOption("themeDependencyFuncs")
deps <- lapply(funcs, function(func) {
deps <- func(theme)
if (length(deps) == 0) return(NULL)
@@ -1405,97 +1379,82 @@ ShinySession <- R6Class(
return(NULL)
}
if (!is.null(private$outputInfo[[name]])) {
return(private$outputInfo[[name]])
}
# The following code will only run the first time this function has been
# called for this output.
tmp_info <- list(name = name)
tmp_info <- private$outputInfo[[name]] %OR% list(name = name)
# cd_names() returns names of all items in clientData, without taking a
# reactive dependency. It is a function and it's memoized, so that we do
# the (relatively) expensive isolate(names(...)) call only when needed,
# and at most one time in this function.
cd_names <- isolate(names(self$clientData))
.cd_names <- NULL
cd_names <- function() {
if (is.null(.cd_names)) {
.cd_names <<- isolate(names(self$clientData))
}
.cd_names
}
# If we don't already have width for this output info, see if it's
# present, and if so, add it.
# Note that all the following clientData values (which are reactiveValues)
# are wrapped in reactive() so that users can take a dependency on particular
# output info (i.e., just depend on width/height, or just depend on bg, fg, etc).
# To put it another way, if getCurrentOutputInfo() simply returned a list of values
# from self$clientData, than anything that calls getCurrentOutputInfo() would take
# a reactive dependency on all of these values.
if (! ("width" %in% names(tmp_info)) ) {
width_name <- paste0("output_", name, "_width")
if (width_name %in% cd_names()) {
tmp_info$width <- reactive({
self$clientData[[width_name]]
})
}
}
if (! ("height" %in% names(tmp_info)) ) {
height_name <- paste0("output_", name, "_height")
if (height_name %in% cd_names()) {
tmp_info$height <- reactive({
self$clientData[[height_name]]
})
}
}
# parseCssColors() currently errors out if you hand it any NAs
# This'll make sure we're always working with a string (and if
# that string isn't a valid CSS color, will return NA)
# https://github.com/rstudio/htmltools/issues/161
parse_css_colors <- function(x) {
htmltools::parseCssColors(x %||% "", mustWork = FALSE)
htmltools::parseCssColors(x %OR% "", mustWork = FALSE)
}
# This function conditionally adds an item to tmp_info (for "width", it
# would create tmp_info$width). It is added _if_ there is an entry in
# clientData like "output_foo_width", where "foo" is the name of the
# output. The first time `tmp_info$width()` is called, it creates a
# reactive expression that reads `clientData$output_foo_width`, saves it,
# then invokes that reactive. On subsequent calls, the reactive already
# exists, so it simply invokes it.
#
# The reason it creates the reactive only on first use is so that it
# doesn't spuriously create reactives.
#
# This function essentially generalizes the code below for names other
# than just "width".
#
# width_name <- paste0("output_", name, "_width")
# if (width_name %in% cd_names()) {
# width_r <- NULL
# tmp_info$width <- function() {
# if (is.null(width_r)) {
# width_r <<- reactive({
# parse_css_colors(self$clientData[[width_name]])
# })
# }
#
# width_r()
# }
# }
add_conditional_reactive <- function(prop, wrapfun = identity) {
force(prop)
force(wrapfun)
prop_name <- paste0("output_", name, "_", prop)
# Only add tmp_info$width if clientData has "output_foo_width"
if (prop_name %in% cd_names) {
r <- NULL
# Turn it into a function that creates a reactive on the first
# invocation of getCurrentOutputInfo()$width() and saves it; future
# invocations of getCurrentOutputInfo()$width() use the existing
# reactive and save it.
tmp_info[[prop]] <- function() {
if (is.null(r)) {
r <<- reactive(label = prop_name, {
wrapfun(self$clientData[[prop_name]])
})
}
r()
}
}
bg <- paste0("output_", name, "_bg")
if (bg %in% cd_names()) {
tmp_info$bg <- reactive({
parse_css_colors(self$clientData[[bg]])
})
}
fg <- paste0("output_", name, "_fg")
if (fg %in% cd_names()) {
tmp_info$fg <- reactive({
parse_css_colors(self$clientData[[fg]])
})
}
# Note that all the following clientData values (which are reactiveValues)
# are wrapped in reactive() so that users can take a dependency on
# particular output info (i.e., just depend on width/height, or just
# depend on bg, fg, etc). To put it another way, if getCurrentOutputInfo()
# simply returned a list of values from self$clientData, than anything
# that calls getCurrentOutputInfo() would take a reactive dependency on
# all of these values.
add_conditional_reactive("width")
add_conditional_reactive("height")
add_conditional_reactive("bg", parse_css_colors)
add_conditional_reactive("fg", parse_css_colors)
add_conditional_reactive("accent", parse_css_colors)
add_conditional_reactive("font")
accent <- paste0("output_", name, "_accent")
if (accent %in% cd_names()) {
tmp_info$accent <- reactive({
parse_css_colors(self$clientData[[accent]])
})
}
font <- paste0("output_", name, "_font")
if (font %in% cd_names()) {
tmp_info$font <- reactive({
self$clientData[[font]]
})
}
private$outputInfo[[name]] <- tmp_info
private$outputInfo[[name]]
@@ -1512,7 +1471,7 @@ ShinySession <- R6Class(
# Warn if trying to enable save-to-server bookmarking on a version of SS,
# SSP, or Connect that doesn't support it.
if (store == "server" && inShinyServer() &&
is.null(getShinyOption("save.interface", default = NULL)))
is.null(getShinyOption("save.interface")))
{
showNotification(
"This app tried to enable saved-to-server bookmarking, but it is not supported by the hosting environment.",
@@ -1928,17 +1887,15 @@ ShinySession <- R6Class(
}
return(httpResponse(
200,
download$contentType %||% getContentType(filename),
download$contentType %OR% getContentType(filename),
# owned=TRUE means tmpdata will be deleted after response completes
list(file=tmpdata, owned=TRUE),
c(
'Content-Disposition' = ifelse(
dlmatches[3] == '',
paste0(
'attachment; filename="',
gsub('(["\\\\])', '\\\\\\1', filename),
'"'
),
'attachment; filename="' %.%
gsub('(["\\\\])', '\\\\\\1', filename) %.% # yes, that many \'s
'"',
'attachment'
),
'Cache-Control'='no-cache')))

View File

@@ -345,17 +345,6 @@ loadSupport <- function(appDir=NULL, renv=new.env(parent=globalenv()), globalren
appDir <- findEnclosingApp(".")
}
descFile <- file.path.ci(appDir, "DESCRIPTION")
if (file.exists(file.path.ci(appDir, "NAMESPACE")) ||
(file.exists(descFile) &&
identical(as.character(read.dcf(descFile, fields = "Type")), "Package")))
{
warning(
"Loading R/ subdirectory for Shiny application, but this directory appears ",
"to contain an R package. Sourcing files in R/ may cause unexpected behavior."
)
}
if (!is.null(globalrenv)){
# Evaluate global.R, if it exists.
globalPath <- file.path.ci(appDir, "global.R")
@@ -569,7 +558,7 @@ as.tags.shiny.appobj <- function(x, ...) {
# jcheng 06/06/2014: Unfortunate copy/paste between this function and
# knit_print.shiny.appobj, but I am trying to make the most conservative
# change possible due to upcoming release.
opts <- x$options %||% list()
opts <- x$options %OR% list()
width <- if (is.null(opts$width)) "100%" else opts$width
height <- if (is.null(opts$height)) "400" else opts$height

View File

@@ -74,7 +74,7 @@ renderPage <- function(ui, showcase=0, testMode=FALSE) {
if (testMode) {
# Add code injection listener if in test mode
shiny_deps[[length(shiny_deps) + 1]] <-
htmlDependency("shiny-testmode", shinyPackageVersion(),
htmlDependency("shiny-testmode", utils::packageVersion("shiny"),
c(href="shared"), script = "shiny-testmode.js")
}
@@ -83,11 +83,12 @@ renderPage <- function(ui, showcase=0, testMode=FALSE) {
}
shinyDependencies <- function() {
version <- utils::packageVersion("shiny")
list(
bslib::bs_dependency_defer(shinyDependencyCSS),
bootstraplib::bs_dependency_defer(shinyDependencyCSS),
htmlDependency(
name = "shiny-javascript",
version = shinyPackageVersion(),
version = version,
src = c(href = "shared"),
script = if (getOption("shiny.minified", TRUE)) "shiny.min.js" else "shiny.js"
)
@@ -95,7 +96,7 @@ shinyDependencies <- function() {
}
shinyDependencyCSS <- function(theme) {
version <- shinyPackageVersion()
version <- utils::packageVersion("shiny")
if (!is_bs_theme(theme)) {
return(htmlDependency(
@@ -110,7 +111,7 @@ shinyDependencyCSS <- function(theme) {
scss_files <- file.path(scss_home, c("bootstrap.scss", "shiny.scss"))
scss_files <- lapply(scss_files, sass::sass_file)
bslib::bs_dependency(
bootstraplib::bs_dependency(
input = scss_files,
theme = theme,
name = "shiny-sass",
@@ -142,7 +143,7 @@ uiHttpHandler <- function(ui, uiPattern = "^/$") {
allowed_methods <- "GET"
if (is.function(ui)) {
allowed_methods <- attr(ui, "http_methods_supported", exact = TRUE) %||% allowed_methods
allowed_methods <- attr(ui, "http_methods_supported", exact = TRUE) %OR% allowed_methods
}
function(req) {

View File

@@ -1,105 +1,34 @@
utils::globalVariables('func', add = TRUE)
utils::globalVariables('func')
#' Mark a function as a render function
#'
#' Should be called by implementers of `renderXXX` functions in order to mark
#' their return values as Shiny render functions, and to provide a hint to Shiny
#' regarding what UI function is most commonly used with this type of render
#' function. This can be used in R Markdown documents to create complete output
#' widgets out of just the render function.
#' Should be called by implementers of `renderXXX` functions in order to
#' mark their return values as Shiny render functions, and to provide a hint to
#' Shiny regarding what UI function is most commonly used with this type of
#' render function. This can be used in R Markdown documents to create complete
#' output widgets out of just the render function.
#'
#' @param uiFunc A function that renders Shiny UI. Must take a single argument:
#' an output ID.
#' @param renderFunc A function that is suitable for assigning to a Shiny output
#' slot.
#' @param outputArgs A list of arguments to pass to the `uiFunc`. Render
#' functions should include `outputArgs = list()` in their own parameter list,
#' and pass through the value to `markRenderFunction`, to allow app authors to
#' customize outputs. (Currently, this is only supported for dynamically
#' generated UIs, such as those created by Shiny code snippets embedded in R
#' Markdown documents).
#' @param cacheHint One of `"auto"`, `FALSE`, or some other information to
#' identify this instance for caching using [bindCache()]. If `"auto"`, it
#' will try to automatically infer caching information. If `FALSE`, do not
#' allow caching for the object. Some render functions (such as [renderPlot])
#' contain internal state that makes them unsuitable for caching.
#' @param cacheWriteHook Used if the render function is passed to `bindCache()`.
#' This is an optional callback function to invoke before saving the value
#' from the render function to the cache. This function must accept one
#' argument, the value returned from `renderFunc`, and should return the value
#' to store in the cache.
#' @param cacheReadHook Used if the render function is passed to `bindCache()`.
#' This is an optional callback function to invoke after reading a value from
#' the cache (if there is a cache hit). The function will be passed one
#' argument, the value retrieved from the cache. This can be useful when some
#' side effect needs to occur for a render function to behave correctly. For
#' example, some render functions call [createWebDependency()] so that Shiny
#' is able to serve JS and CSS resources.
#' functions should include `outputArgs = list()` in their own parameter
#' list, and pass through the value to `markRenderFunction`, to allow
#' app authors to customize outputs. (Currently, this is only supported for
#' dynamically generated UIs, such as those created by Shiny code snippets
#' embedded in R Markdown documents).
#' @return The `renderFunc` function, with annotations.
#'
#' @seealso [createRenderFunction()], [quoToFunction()]
#' @export
markRenderFunction <- function(
uiFunc,
renderFunc,
outputArgs = list(),
cacheHint = "auto",
cacheWriteHook = NULL,
cacheReadHook = NULL
) {
force(renderFunc)
markRenderFunction <- function(uiFunc, renderFunc, outputArgs = list()) {
# a mutable object that keeps track of whether `useRenderFunction` has been
# executed (this usually only happens when rendering Shiny code snippets in
# an interactive R Markdown document); its initial value is FALSE
hasExecuted <- Mutable$new()
hasExecuted$set(FALSE)
if (is.null(uiFunc)) {
uiFunc <- function(id) {
pre(
"No UI/output function provided for render function. ",
"Please see ?shiny::markRenderFunction and ?shiny::createRenderFunction."
)
}
}
if (identical(cacheHint, "auto")) {
origUserFunc <- attr(renderFunc, "wrappedFunc", exact = TRUE)
# The result could be NULL, but don't warn now because it'll only affect
# users if they try to use caching. We'll warn when someone calls
# bindCache() on this object.
if (is.null(origUserFunc)) {
cacheHint <- NULL
} else {
# Add in the wrapper render function and they output function, because
# they can be useful for distinguishing two renderX functions that receive
# the same user expression but do different things with them (like
# renderText and renderPrint).
cacheHint <- list(
origUserFunc = origUserFunc,
renderFunc = renderFunc,
outputFunc = uiFunc
)
}
}
if (!is.null(cacheHint) && !is_false(cacheHint)) {
if (!is.list(cacheHint)) {
cacheHint <- list(cacheHint)
}
# For functions, remove the env and source refs because they can cause
# spurious differences.
# For expressions, remove source refs.
# For everything else, do nothing.
cacheHint <- lapply(cacheHint, function(x) {
if (is.function(x)) formalsAndBody(x)
else if (is.language(x)) zap_srcref(x)
else x
})
}
wrappedRenderFunc <- function(...) {
origRenderFunc <- renderFunc
renderFunc <- function(...) {
# if the user provided something through `outputArgs` BUT the
# `useRenderFunction` was not executed, then outputArgs will be ignored,
# so throw a warning to let user know the correct usage
@@ -112,20 +41,15 @@ markRenderFunction <- function(
# stop warning from happening again for the same object
hasExecuted$set(TRUE)
}
if (is.null(formals(renderFunc))) renderFunc()
else renderFunc(...)
if (is.null(formals(origRenderFunc))) origRenderFunc()
else origRenderFunc(...)
}
structure(
wrappedRenderFunc,
class = c("shiny.render.function", "function"),
outputFunc = uiFunc,
outputArgs = outputArgs,
hasExecuted = hasExecuted,
cacheHint = cacheHint,
cacheWriteHook = cacheWriteHook,
cacheReadHook = cacheReadHook
)
structure(renderFunc,
class = c("shiny.render.function", "function"),
outputFunc = uiFunc,
outputArgs = outputArgs,
hasExecuted = hasExecuted)
}
#' @export
@@ -135,9 +59,6 @@ print.shiny.render.function <- function(x, ...) {
#' Implement render functions
#'
#' This function is a wrapper for [markRenderFunction()] which provides support
#' for async computation via promises.
#'
#' @param func A function without parameters, that returns user data. If the
#' returned value is a promise, then the render function will proceed in async
#' mode.
@@ -149,63 +70,34 @@ print.shiny.render.function <- function(x, ...) {
#' @param outputFunc The UI function that is used (or most commonly used) with
#' this render function. This can be used in R Markdown documents to create
#' complete output widgets out of just the render function.
#' @inheritParams markRenderFunction
#' @param outputArgs A list of arguments to pass to the `outputFunc`.
#' Render functions should include `outputArgs = list()` in their own
#' parameter list, and pass through the value as this argument, to allow app
#' authors to customize outputs. (Currently, this is only supported for
#' dynamically generated UIs, such as those created by Shiny code snippets
#' embedded in R Markdown documents).
#' @return An annotated render function, ready to be assigned to an
#' `output` slot.
#'
#' @seealso [quoToFunction()], [markRenderFunction()].
#'
#' @examples
#' # A very simple render function
#' renderTriple <- function(x) {
#' x <- substitute(x)
#' if (!rlang::is_quosure(x)) {
#' x <- rlang::new_quosure(x, env = parent.frame())
#' }
#' func <- quoToFunction(x, "renderTriple")
#'
#' createRenderFunction(
#' func,
#' transform = function(value, session, name, ...) {
#' paste(rep(value, 3), collapse=", ")
#' },
#' outputFunc = textOutput
#' )
#' }
#'
#' # Test render function from the console
#' a <- 1
#' r <- renderTriple({ a + 1 })
#' a <- 2
#' r()
#' @export
createRenderFunction <- function(
func,
transform = function(value, session, name, ...) value,
outputFunc = NULL,
outputArgs = NULL,
cacheHint = "auto",
cacheWriteHook = NULL,
cacheReadHook = NULL
func, transform = function(value, session, name, ...) value,
outputFunc = NULL, outputArgs = NULL
) {
renderFunc <- function(shinysession, name, ...) {
hybrid_chain(
func(),
function(value) {
transform(value, shinysession, name, ...)
function(value, .visible) {
transform(setVisible(value, .visible), shinysession, name, ...)
}
)
}
# Hoist func's wrappedFunc attribute into renderFunc, so that when we pass
# renderFunc on to markRenderFunction, it is able to find the original user
# function.
if (identical(cacheHint, "auto")) {
attr(renderFunc, "wrappedFunc") <- attr(func, "wrappedFunc", exact = TRUE)
}
markRenderFunction(outputFunc, renderFunc, outputArgs, cacheHint,
cacheWriteHook, cacheReadHook)
if (!is.null(outputFunc))
markRenderFunction(outputFunc, renderFunc, outputArgs = outputArgs)
else
renderFunc
}
useRenderFunction <- function(renderFunc, inline = FALSE) {
@@ -248,22 +140,6 @@ as.tags.shiny.render.function <- function(x, ..., inline = FALSE) {
useRenderFunction(x, inline = inline)
}
# Get relevant attributes from a render function object.
renderFunctionAttributes <- function(x) {
attrs <- c("outputFunc", "outputArgs", "hasExecuted", "cacheHint")
names(attrs) <- attrs
lapply(attrs, function(name) attr(x, name, exact = TRUE))
}
# Add a named list of attributes to an object
addAttributes <- function(x, attrs) {
nms <- names(attrs)
for (i in seq_along(attrs)) {
attr(x, nms[i]) <- attrs[[i]]
}
x
}
#' Mark a render function with attributes that will be used by the output
#'
@@ -398,10 +274,8 @@ markOutputAttrs <- function(renderFunc, snapshotExclude = NULL,
#' shinyApp(ui, server)
#' }
renderImage <- function(expr, env=parent.frame(), quoted=FALSE,
deleteFile, outputArgs=list())
{
expr <- get_quosure(expr, env, quoted)
func <- quoToFunction(expr, "renderImage")
deleteFile, outputArgs=list()) {
installExprFunction(expr, "func", env, quoted)
# missing() must be used directly within the function with the given arg
if (missing(deleteFile)) {
@@ -450,7 +324,7 @@ renderImage <- function(expr, env=parent.frame(), quoted=FALSE,
}
# If contentType not specified, autodetect based on extension
contentType <- imageinfo$contentType %||% getContentType(imageinfo$src)
contentType <- imageinfo$contentType %OR% getContentType(imageinfo$src)
# Extra values are everything in imageinfo except 'src' and 'contentType'
extra_attr <- imageinfo[!names(imageinfo) %in% c('src', 'contentType')]
@@ -459,10 +333,7 @@ renderImage <- function(expr, env=parent.frame(), quoted=FALSE,
c(src = session$fileUrl(name, file=imageinfo$src, contentType=contentType),
extra_attr)
},
imageOutput,
outputArgs,
cacheHint = FALSE
)
imageOutput, outputArgs)
}
# TODO: If we ever take a dependency on fs, it'd be great to replace this with
@@ -534,10 +405,8 @@ isTemp <- function(path, tempDir = tempdir(), mustExist) {
#' @example res/text-example.R
#' @export
renderPrint <- function(expr, env = parent.frame(), quoted = FALSE,
width = getOption('width'), outputArgs=list())
{
expr <- get_quosure(expr, env, quoted)
func <- quoToFunction(expr, "renderPrint")
width = getOption('width'), outputArgs=list()) {
installExprFunction(expr, "func", env, quoted)
# Set a promise domain that sets the console width
# and captures output
@@ -550,12 +419,12 @@ renderPrint <- function(expr, env = parent.frame(), quoted = FALSE,
{
promises::with_promise_domain(domain, func())
},
function(value) {
res <- withVisible(value)
if (res$visible) {
cat(file = domain$conn, paste(utils::capture.output(res$value, append = TRUE), collapse = "\n"))
function(value, .visible) {
if (.visible) {
cat(file = domain$conn, paste(utils::capture.output(value, append = TRUE), collapse = "\n"))
}
paste(readLines(domain$conn, warn = FALSE), collapse = "\n")
res <- paste(readLines(domain$conn, warn = FALSE), collapse = "\n")
res
},
finally = function() {
close(domain$conn)
@@ -563,15 +432,7 @@ renderPrint <- function(expr, env = parent.frame(), quoted = FALSE,
)
}
markRenderFunction(
verbatimTextOutput,
renderFunc,
outputArgs,
cacheHint = list(
label = "renderPrint",
origUserExpr = get_expr(expr)
)
)
markRenderFunction(verbatimTextOutput, renderFunc, outputArgs = outputArgs)
}
createRenderPrintPromiseDomain <- function(width) {
@@ -621,17 +482,14 @@ createRenderPrintPromiseDomain <- function(width) {
#' @rdname renderPrint
renderText <- function(expr, env=parent.frame(), quoted=FALSE,
outputArgs=list(), sep=" ") {
expr <- get_quosure(expr, env, quoted)
func <- quoToFunction(expr, "renderText")
installExprFunction(expr, "func", env, quoted)
createRenderFunction(
func,
function(value, session, name, ...) {
paste(utils::capture.output(cat(value, sep=sep)), collapse="\n")
},
textOutput,
outputArgs
textOutput, outputArgs
)
}
@@ -672,11 +530,9 @@ renderText <- function(expr, env=parent.frame(), quoted=FALSE,
#' shinyApp(ui, server)
#' }
#'
renderUI <- function(expr, env = parent.frame(), quoted = FALSE,
outputArgs = list())
{
expr <- get_quosure(expr, env, quoted)
func <- quoToFunction(expr, "renderUI")
renderUI <- function(expr, env=parent.frame(), quoted=FALSE,
outputArgs=list()) {
installExprFunction(expr, "func", env, quoted)
createRenderFunction(
func,
@@ -686,8 +542,7 @@ renderUI <- function(expr, env = parent.frame(), quoted = FALSE,
processDeps(result, shinysession)
},
uiOutput,
outputArgs
uiOutput, outputArgs
)
}
@@ -748,7 +603,7 @@ downloadHandler <- function(filename, content, contentType=NA, outputArgs=list()
shinysession$registerDownload(name, filename, contentType, content)
}
snapshotExclude(
markRenderFunction(downloadButton, renderFunc, outputArgs, cacheHint = FALSE)
markRenderFunction(downloadButton, renderFunc, outputArgs = outputArgs)
)
}
@@ -819,10 +674,8 @@ downloadHandler <- function(filename, content, contentType=NA, outputArgs=list()
renderDataTable <- function(expr, options = NULL, searchDelay = 500,
callback = 'function(oTable) {}', escape = TRUE,
env = parent.frame(), quoted = FALSE,
outputArgs=list())
{
expr <- get_quosure(expr, env, quoted)
func <- quoToFunction(expr, "renderDataTable")
outputArgs=list()) {
installExprFunction(expr, "func", env, quoted)
renderFunc <- function(shinysession, name, ...) {
if (is.function(options)) options <- options()
@@ -856,8 +709,7 @@ renderDataTable <- function(expr, options = NULL, searchDelay = 500,
)
}
renderFunc <- markRenderFunction(dataTableOutput, renderFunc, outputArgs,
cacheHint = FALSE)
renderFunc <- markRenderFunction(dataTableOutput, renderFunc, outputArgs = outputArgs)
renderFunc <- snapshotPreprocessOutput(renderFunc, function(value) {
# Remove the action field so that it's not saved in test snapshots. It

View File

@@ -1,110 +0,0 @@
# Given a list of quosures, return a function that will evaluate them and return
# a list of resulting values. If the list contains a single expression, unwrap
# it from the list.
quos_to_func <- function(qs) {
if (length(qs) == 0) {
stop("Need at least one item in `...` to use as cache key or event.")
}
if (length(qs) == 1) {
# Special case for one quosure. This is needed for async to work -- that is,
# when the quosure returns a promise. It needs to not be wrapped into a list
# for the hybrid_chain stuff to detect that it's a promise. (Plus, it's not
# even clear what it would mean to mix promises and non-promises in the
# key.)
qs <- qs[[1]]
function() {
eval_tidy(qs)
}
} else {
function() {
lapply(qs, eval_tidy)
}
}
}
# Given a list of quosures, return a string representation of the expressions.
#
# qs <- list(quo(a+1), quo({ b+2; b + 3 }))
# quos_to_label(qs)
# #> [1] "a + 1, {\n b + 2\n b + 3\n}"
quos_to_label <- function(qs) {
res <- lapply(qs, function(q) {
paste(deparse(get_expr(q)), collapse = "\n")
})
paste(res, collapse = ", ")
}
# Get the formals and body for a function, without source refs. This is used for
# consistent hashing of the function.
formalsAndBody <- function(x) {
if (is.null(x)) {
return(list())
}
list(
formals = formals(x),
body = body(zap_srcref(x))
)
}
# This function is to be called from functions like `reactive()`, `observe()`,
# and the various render functions. It handles the following cases:
# - The typical case where x is an unquoted expression, and `env` and `quoted`
# are not used.
# - New-style metaprogramming cases, where rlang::inject() is used to inline a
# quosure into the AST, as in `inject(reactive(!!x))`.
# - Old-style metaprogramming cases, where `env` and/or `quoted` are used.
#
# Much of the complexity is handling old-style metaprogramming cases. The code
# in this function is more complicated because it needs to look at unevaluated
# expressions in the _calling_ function. If this code were put directly in the
# calling function, it would look like this:
#
# if (!missing(env) || !missing(quoted)) {
# deprecatedEnvQuotedMessage()
# if (!quoted) x <- substitute(x)
# x <- new_quosure(x, env)
#
# } else {
# x <- substitute(x)
# if (!is_quosure(x)) {
# x <- new_quosure(x, env = parent.frame())
# }
# }
#
# In the future, the calling functions will not need to have the `env` and
# `quoted` arguments -- `rlang::inject()` and quosures can be used instead.
# Instead of using this function, `get_quosure()`, the caller can instead use
# just the following code:
#
# x <- substitute(x)
# if (!is_quosure(x)) {
# x <- new_quosure(x, env = parent.frame())
# }
#
get_quosure <- function(x, env, quoted) {
if (!eval(substitute(missing(env)), parent.frame()) ||
!eval(substitute(missing(quoted)), parent.frame()))
{
deprecatedEnvQuotedMessage()
if (!quoted) {
x <- eval(substitute(substitute(x)), parent.frame())
}
x <- new_quosure(x, env)
} else {
x <- eval(substitute(substitute(x)), parent.frame())
# At this point, x can be a quosure if rlang::inject() is used, but the
# typical case is that x is not a quosure.
if (!is_quosure(x)) {
x <- new_quosure(x, env = parent.frame(2L))
}
}
x
}

195
R/utils.R
View File

@@ -113,6 +113,24 @@ isWholeNum <- function(x, tol = .Machine$double.eps^0.5) {
abs(x - round(x)) < tol
}
`%OR%` <- function(x, y) {
if (is.null(x) || isTRUE(is.na(x)))
y
else
x
}
`%AND%` <- function(x, y) {
if (!is.null(x) && !isTRUE(is.na(x)))
if (!is.null(y) && !isTRUE(is.na(y)))
return(y)
return(NULL)
}
`%.%` <- function(x, y) {
paste(x, y, sep='')
}
# Given a vector or list, drop all the NULL items in it
dropNulls <- function(x) {
x[!vapply(x, is.null, FUN.VALUE=logical(1))]
@@ -202,11 +220,6 @@ sort_c <- function(x, ...) {
sort(x, method = "radix", ...)
}
# Base R isFALSE function was added in R 3.5.0.
is_false <- function(x) {
identical(x, FALSE)
}
# Wrapper around list2env with a NULL check. In R <3.2.0, if an empty unnamed
# list is passed to list2env(), it errors. But an empty named list is OK. For
# R >=3.2.0, this wrapper is not necessary.
@@ -414,8 +427,7 @@ makeFunction <- function(args = pairlist(), body, env = parent.frame()) {
#' Convert an expression to a function
#'
#' This is to be called from another function, because it will attempt to get
#' an unquoted expression from two calls back. Note: as of Shiny 1.6.0, it is
#' recommended to use [quoToFunction()] instead.
#' an unquoted expression from two calls back.
#'
#' If expr is a quoted expression, then this just converts it to a function.
#' If expr is a function, then this simply returns expr (and prints a
@@ -473,8 +485,7 @@ exprToFunction <- function(expr, env=parent.frame(), quoted=FALSE) {
#' Install an expression as a function
#'
#' Installs an expression in the given environment as a function, and registers
#' debug hooks so that breakpoints may be set in the function. Note: as of
#' Shiny 1.6.0, it is recommended to use [quoToFunction()] instead.
#' debug hooks so that breakpoints may be set in the function.
#'
#' This function can replace `exprToFunction` as follows: we may use
#' `func <- exprToFunction(expr)` if we do not want the debug hooks, or
@@ -520,48 +531,6 @@ installExprFunction <- function(expr, name, eval.env = parent.frame(2),
assign(name, func, envir = assign.env)
}
#' Convert a quosure to a function for a Shiny render function
#'
#' This takes a quosure and label, and wraps them into a function that should be
#' passed to [createRenderFunction()] or [markRenderFunction()].
#'
#' This function was added in Shiny 1.6.0. Previously, it was recommended to use
#' [installExprFunction()] or [exprToFunction()] in render functions, but now we
#' recommend using [quoToFunction()], because it does not require `env` and
#' `quoted` arguments -- that information is captured by quosures provided by
#' \pkg{rlang}.
#'
#' @param q A quosure.
#' @inheritParams installExprFunction
#' @seealso [createRenderFunction()] for example usage.
#'
#' @export
quoToFunction <- function(q, label, ..stacktraceon = FALSE) {
q <- as_quosure(q)
# Use new_function() instead of as_function(), because as_function() adds an
# extra parent environment. (This may not actually be a problem, though.)
func <- new_function(NULL, get_expr(q), get_env(q))
wrapFunctionLabel(func, label, ..stacktraceon = ..stacktraceon)
}
# Utility function for creating a debugging label, given an expression.
# `expr` is a quoted expression.
# `function_name` is the name of the calling function.
# `label` is an optional user-provided label. If NULL, it will be inferred.
exprToLabel <- function(expr, function_name, label = NULL) {
srcref <- attr(expr, "srcref", exact = TRUE)
if (is.null(label)) {
label <- rexprSrcrefToLabel(
srcref[[1]],
sprintf('%s(%s)', function_name, paste(deparse(expr), collapse = '\n'))
)
}
if (length(srcref) >= 2) attr(label, "srcref") <- srcref[[2]]
attr(label, "srcfile") <- srcFileOfRef(srcref[[1]])
label
}
#' Parse a GET query string from a URL
#'
#' Returns a named list of key-value pairs.
@@ -662,6 +631,37 @@ shinyCallingHandlers <- function(expr) {
)
}
#' Print message for deprecated functions in Shiny
#'
#' To disable these messages, use `options(shiny.deprecation.messages=FALSE)`.
#'
#' @param new Name of replacement function.
#' @param msg Message to print. If used, this will override the default message.
#' @param old Name of deprecated function.
#' @param version The last version of Shiny before the item was deprecated.
#' @keywords internal
shinyDeprecated <- function(new=NULL, msg=NULL,
old=as.character(sys.call(sys.parent()))[1L],
version = NULL) {
if (getOption("shiny.deprecation.messages") %OR% TRUE == FALSE)
return(invisible())
if (is.null(msg)) {
msg <- paste(old, "is deprecated.")
if (!is.null(new)) {
msg <- paste(msg, "Please use", new, "instead.",
"To disable this message, run options(shiny.deprecation.messages=FALSE)")
}
}
if (!is.null(version)) {
msg <- paste0(msg, " (Last used in version ", version, ")")
}
# Similar to .Deprecated(), but print a message instead of warning
message(msg)
}
#' Register a function with the debugger (if one is active).
#'
@@ -1587,19 +1587,14 @@ URLencode <- function(value, reserved = FALSE) {
if (reserved) encodeURIComponent(value) else encodeURI(value)
}
# Make sure user-supplied dates are either NULL or can be coerced to a
# yyyy-mm-dd formatted string. If a date is specified, this function returns a
# string for consistency across locales. Also, `as.Date()` is used to coerce
# strings to date objects so that strings like "2016-08-9" are expanded to
# "2016-08-09". If any of the values result in error or NA, then the input
# `date` is returned unchanged.
# Make user-supplied dates are either NULL or can be coerced
# to a yyyy-mm-dd formatted string. If a date is specified, this
# function returns a string for consistency across locales.
# Also, `as.Date()` is used to coerce strings to date objects
# so that strings like "2016-08-9" are expanded to "2016-08-09"
dateYMD <- function(date = NULL, argName = "value") {
if (!length(date)) return(NULL)
tryCatch({
res <- format(as.Date(date), "%Y-%m-%d")
if (any(is.na(res))) stop()
date <- res
},
tryCatch(date <- format(as.Date(date), "%Y-%m-%d"),
error = function(e) {
warning(
"Couldn't coerce the `", argName,
@@ -1622,17 +1617,18 @@ wrapFunctionLabel <- function(func, name, ..stacktraceon = FALSE) {
assign(name, func, environment())
registerDebugHook(name, environment(), name)
if (..stacktraceon) {
# We need to wrap the `...` in `!!quote(...)` so that R CMD check won't
# complain about "... may be used in an incorrect context"
body <- expr({ ..stacktraceon..((!!name)(!!quote(...))) })
} else {
body <- expr({ (!!name)(!!quote(...)) })
}
relabelWrapper <- new_function(pairlist2(... =), body, environment())
relabelWrapper <- eval(substitute(
function(...) {
# This `f` gets renamed to the value of `name`. Note that it may not
# print as the new name, because of source refs stored in the function.
if (..stacktraceon)
..stacktraceon..(f(...))
else
f(...)
},
list(f = as.name(name))
))
# Preserve the original function that was passed in; is used for caching.
attr(relabelWrapper, "wrappedFunc") <- func
relabelWrapper
}
@@ -1692,23 +1688,19 @@ hybrid_chain <- function(expr, ..., catch = NULL, finally = NULL,
if (promises::is.promising(result$value)) {
# Purposefully NOT including domain (nor replace), as we're already in
# the domain at this point
p <- promise_chain(valueWithVisible(result), ..., catch = catch, finally = finally)
p <- promise_chain(setVisible(result), ..., catch = catch, finally = finally)
runFinally <- FALSE
p
} else {
result <- Reduce(
function(v, func) {
if (v$visible) {
withVisible(func(v$value))
} else {
withVisible(func(invisible(v$value)))
}
},
list(...),
result
)
result <- Reduce(function(v, func) {
if (".visible" %in% names(formals(func))) {
withVisible(func(v$value, .visible = v$visible))
} else {
withVisible(func(v$value))
}
}, list(...), result)
valueWithVisible(result)
setVisible(result)
}
})
},
@@ -1729,12 +1721,23 @@ hybrid_chain <- function(expr, ..., catch = NULL, finally = NULL,
}
}
# Given a list with items named `value` and `visible`, return `x$value` either
# visibly, or invisibly, depending on the value of `x$visible`.
valueWithVisible <- function(x) {
if (x$visible) x$value else invisible(x$value)
}
# Returns `value` with either `invisible()` applied or not, depending on the
# value of `visible`.
#
# If the `visible` is missing, then `value` should be a list as returned from
# `withVisible()`, and that visibility will be applied.
setVisible <- function(value, visible) {
if (missing(visible)) {
visible <- value$visible
value <- value$value
}
if (!visible) {
invisible(value)
} else {
(value)
}
}
createVarPromiseDomain <- function(env, name, value) {
force(env)
@@ -1889,15 +1892,3 @@ is_available <- function(package, version = NULL) {
}
installed && isTRUE(utils::packageVersion(package) >= version)
}
# cached version of utils::packageVersion("shiny")
shinyPackageVersion <- local({
version <- NULL
function() {
if (is.null(version)) {
version <<- utils::packageVersion("shiny")
}
version
}
})

View File

@@ -46,6 +46,18 @@ remotes::install_github("rstudio/shiny")
To learn more we highly recommend you check out the [Shiny Tutorial](http://shiny.rstudio.com/tutorial/). The tutorial explains the framework in-depth, walks you through building a simple application, and includes extensive annotated examples.
## Bootstrap 3 migration
Shiny versions 0.10.2.2 and below used the Bootstrap 2 web framework. After 0.10.2.2, Shiny switched to Bootstrap 3. For most users, the upgrade should be seamless. However, if you have have customized your HTML-generating code to use features specific to Bootstrap 2, you may need to update your code to work with Bootstrap 3.
If you do not wish to update your code at this time, you can use the [shinybootstrap2](https://github.com/rstudio/shinybootstrap2) package for backward compatibility.
If you prefer to install an older version of Shiny, you can do it using the devtools package:
```R
devtools::install_version("shiny", version = "0.10.2.2")
```
## Development notes
The Javascript code in Shiny is minified using tools that run on Node.js. See the tools/ directory for more information.

View File

@@ -1,2 +1,3 @@
library(shinytest)
expect_pass(testApp("../", suffix = osName()))
shinytest::testApp("../")

View File

@@ -109,7 +109,7 @@
}
.datepicker table tr td.day:hover, .datepicker table tr td.focused {
color: #000;
color: #212529;
background: #e9e9ea;
cursor: pointer;
}
@@ -121,37 +121,40 @@
}
.datepicker table tr td.highlighted {
color: #000;
color: #212529;
background-color: #d1ecf1;
border-color: #83ccd9;
border-radius: 0;
}
.datepicker table tr td.highlighted:focus, .datepicker table tr td.highlighted.focus {
color: #000;
background-color: #bcd4d9;
border-color: #6299a3;
color: #212529;
background-color: #bfd8dd;
border-color: #6aa2ad;
}
.datepicker table tr td.highlighted:hover {
color: #000;
background-color: #697679;
border-color: #73b3bf;
color: #212529;
background-color: #79898d;
border-color: #77b8c4;
}
.datepicker table tr td.highlighted:active, .datepicker table tr td.highlighted.active {
color: #000;
background-color: #bcd4d9;
border-color: #73b3bf;
color: #212529;
background-color: #bfd8dd;
border-color: #77b8c4;
}
.datepicker table tr td.highlighted:active:hover, .datepicker table tr td.highlighted:active:focus, .datepicker table tr td.highlighted.focus:active, .datepicker table tr td.highlighted.active:hover, .datepicker table tr td.highlighted.active:focus, .datepicker table tr td.highlighted.active.focus {
color: #000;
background-color: #adc4c8;
border-color: #6299a3;
.datepicker table tr td.highlighted:active:hover, .datepicker table tr td.highlighted:active:focus, .datepicker table tr td.highlighted:active.focus, .datepicker table tr td.highlighted.active:hover, .datepicker table tr td.highlighted.active:focus, .datepicker table tr td.highlighted.active.focus {
color: #212529;
background-color: #b3cacf;
border-color: #6aa2ad;
}
.datepicker table tr td.highlighted.disabled:hover, .datepicker table tr td.highlighted.disabled:focus, .datepicker table tr td.highlighted.disabled.focus, .datepicker table tr td.highlighted[disabled]:hover, .datepicker table tr td.highlighted[disabled]:focus, .datepicker table tr td.highlighted.focus[disabled], fieldset[disabled] .datepicker table tr td.highlighted:hover, fieldset[disabled] .datepicker table tr td.highlighted:focus, fieldset[disabled] .datepicker table tr td.highlighted.focus {
.datepicker table tr td.highlighted.disabled:hover, .datepicker table tr td.highlighted.disabled:focus, .datepicker table tr td.highlighted.disabled.focus, .datepicker table tr td.highlighted[disabled]:hover, .datepicker table tr td.highlighted[disabled]:focus, .datepicker table tr td.highlighted[disabled].focus,
fieldset[disabled] .datepicker table tr td.highlighted:hover,
fieldset[disabled] .datepicker table tr td.highlighted:focus,
fieldset[disabled] .datepicker table tr td.highlighted.focus {
background-color: #d1ecf1;
border-color: #83ccd9;
}
@@ -166,36 +169,39 @@
}
.datepicker table tr td.today {
color: #000;
color: #212529;
background-color: #ffdb99;
border-color: #ffb733;
}
.datepicker table tr td.today:focus, .datepicker table tr td.today.focus {
color: #000;
background-color: #e6c58a;
border-color: #bf8926;
color: #212529;
background-color: #e9c98e;
border-color: #c89331;
}
.datepicker table tr td.today:hover {
color: #000;
background-color: #806e4d;
border-color: #e0a12d;
color: #212529;
background-color: #908061;
border-color: #e4a532;
}
.datepicker table tr td.today:active, .datepicker table tr td.today.active {
color: #000;
background-color: #e6c58a;
border-color: #e0a12d;
color: #212529;
background-color: #e9c98e;
border-color: #e4a532;
}
.datepicker table tr td.today:active:hover, .datepicker table tr td.today:active:focus, .datepicker table tr td.today.focus:active, .datepicker table tr td.today.active:hover, .datepicker table tr td.today.active:focus, .datepicker table tr td.today.active.focus {
color: #000;
background-color: #d4b67f;
border-color: #bf8926;
.datepicker table tr td.today:active:hover, .datepicker table tr td.today:active:focus, .datepicker table tr td.today:active.focus, .datepicker table tr td.today.active:hover, .datepicker table tr td.today.active:focus, .datepicker table tr td.today.active.focus {
color: #212529;
background-color: #d9bc86;
border-color: #c89331;
}
.datepicker table tr td.today.disabled:hover, .datepicker table tr td.today.disabled:focus, .datepicker table tr td.today.disabled.focus, .datepicker table tr td.today[disabled]:hover, .datepicker table tr td.today[disabled]:focus, .datepicker table tr td.today.focus[disabled], fieldset[disabled] .datepicker table tr td.today:hover, fieldset[disabled] .datepicker table tr td.today:focus, fieldset[disabled] .datepicker table tr td.today.focus {
.datepicker table tr td.today.disabled:hover, .datepicker table tr td.today.disabled:focus, .datepicker table tr td.today.disabled.focus, .datepicker table tr td.today[disabled]:hover, .datepicker table tr td.today[disabled]:focus, .datepicker table tr td.today[disabled].focus,
fieldset[disabled] .datepicker table tr td.today:hover,
fieldset[disabled] .datepicker table tr td.today:focus,
fieldset[disabled] .datepicker table tr td.today.focus {
background-color: #ffdb99;
border-color: #ffb733;
}
@@ -210,37 +216,40 @@
}
.datepicker table tr td.range {
color: #000;
color: #212529;
background-color: #e9e9ea;
border-color: #b5b5b8;
border-radius: 0;
}
.datepicker table tr td.range:focus, .datepicker table tr td.range.focus {
color: #000;
background-color: #d2d2d3;
border-color: #88888a;
color: #212529;
background-color: #d5d5d7;
border-color: #909194;
}
.datepicker table tr td.range:hover {
color: #000;
background-color: #757575;
border-color: #9f9fa2;
color: #212529;
background-color: #85878a;
border-color: #a3a4a7;
}
.datepicker table tr td.range:active, .datepicker table tr td.range.active {
color: #000;
background-color: #d2d2d3;
border-color: #9f9fa2;
color: #212529;
background-color: #d5d5d7;
border-color: #a3a4a7;
}
.datepicker table tr td.range:active:hover, .datepicker table tr td.range:active:focus, .datepicker table tr td.range.focus:active, .datepicker table tr td.range.active:hover, .datepicker table tr td.range.active:focus, .datepicker table tr td.range.active.focus {
color: #000;
background-color: #c1c1c2;
border-color: #88888a;
.datepicker table tr td.range:active:hover, .datepicker table tr td.range:active:focus, .datepicker table tr td.range:active.focus, .datepicker table tr td.range.active:hover, .datepicker table tr td.range.active:focus, .datepicker table tr td.range.active.focus {
color: #212529;
background-color: #c7c8c9;
border-color: #909194;
}
.datepicker table tr td.range.disabled:hover, .datepicker table tr td.range.disabled:focus, .datepicker table tr td.range.disabled.focus, .datepicker table tr td.range[disabled]:hover, .datepicker table tr td.range[disabled]:focus, .datepicker table tr td.range.focus[disabled], fieldset[disabled] .datepicker table tr td.range:hover, fieldset[disabled] .datepicker table tr td.range:focus, fieldset[disabled] .datepicker table tr td.range.focus {
.datepicker table tr td.range.disabled:hover, .datepicker table tr td.range.disabled:focus, .datepicker table tr td.range.disabled.focus, .datepicker table tr td.range[disabled]:hover, .datepicker table tr td.range[disabled]:focus, .datepicker table tr td.range[disabled].focus,
fieldset[disabled] .datepicker table tr td.range:hover,
fieldset[disabled] .datepicker table tr td.range:focus,
fieldset[disabled] .datepicker table tr td.range.focus {
background-color: #e9e9ea;
border-color: #b5b5b8;
}
@@ -255,36 +264,39 @@
}
.datepicker table tr td.range.highlighted {
color: #000;
color: #212529;
background-color: #ddebee;
border-color: #99c3cc;
}
.datepicker table tr td.range.highlighted:focus, .datepicker table tr td.range.highlighted.focus {
color: #000;
background-color: #c7d4d6;
border-color: #739299;
color: #212529;
background-color: #cad7da;
border-color: #7b9ca3;
}
.datepicker table tr td.range.highlighted:hover {
color: #000;
background-color: #6f7677;
border-color: #87acb4;
color: #212529;
background-color: #7f888c;
border-color: #8bb0b8;
}
.datepicker table tr td.range.highlighted:active, .datepicker table tr td.range.highlighted.active {
color: #000;
background-color: #c7d4d6;
border-color: #87acb4;
color: #212529;
background-color: #cad7da;
border-color: #8bb0b8;
}
.datepicker table tr td.range.highlighted:active:hover, .datepicker table tr td.range.highlighted:active:focus, .datepicker table tr td.range.highlighted.focus:active, .datepicker table tr td.range.highlighted.active:hover, .datepicker table tr td.range.highlighted.active:focus, .datepicker table tr td.range.highlighted.active.focus {
color: #000;
background-color: #b7c3c6;
border-color: #739299;
.datepicker table tr td.range.highlighted:active:hover, .datepicker table tr td.range.highlighted:active:focus, .datepicker table tr td.range.highlighted:active.focus, .datepicker table tr td.range.highlighted.active:hover, .datepicker table tr td.range.highlighted.active:focus, .datepicker table tr td.range.highlighted.active.focus {
color: #212529;
background-color: #bdc9cd;
border-color: #7b9ca3;
}
.datepicker table tr td.range.highlighted.disabled:hover, .datepicker table tr td.range.highlighted.disabled:focus, .datepicker table tr td.range.highlighted.disabled.focus, .datepicker table tr td.range.highlighted[disabled]:hover, .datepicker table tr td.range.highlighted[disabled]:focus, .datepicker table tr td.range.highlighted.focus[disabled], fieldset[disabled] .datepicker table tr td.range.highlighted:hover, fieldset[disabled] .datepicker table tr td.range.highlighted:focus, fieldset[disabled] .datepicker table tr td.range.highlighted.focus {
.datepicker table tr td.range.highlighted.disabled:hover, .datepicker table tr td.range.highlighted.disabled:focus, .datepicker table tr td.range.highlighted.disabled.focus, .datepicker table tr td.range.highlighted[disabled]:hover, .datepicker table tr td.range.highlighted[disabled]:focus, .datepicker table tr td.range.highlighted[disabled].focus,
fieldset[disabled] .datepicker table tr td.range.highlighted:hover,
fieldset[disabled] .datepicker table tr td.range.highlighted:focus,
fieldset[disabled] .datepicker table tr td.range.highlighted.focus {
background-color: #ddebee;
border-color: #99c3cc;
}
@@ -299,36 +311,39 @@
}
.datepicker table tr td.range.today {
color: #000;
color: #212529;
background-color: #f4c775;
border-color: #eca117;
}
.datepicker table tr td.range.today:focus, .datepicker table tr td.range.today.focus {
color: #000;
background-color: #dcb369;
border-color: #b17811;
color: #212529;
background-color: #dfb76d;
border-color: #ba821b;
}
.datepicker table tr td.range.today:hover {
color: #000;
background-color: #7a643b;
border-color: #d08d14;
color: #212529;
background-color: #8b764f;
border-color: #d49219;
}
.datepicker table tr td.range.today:active, .datepicker table tr td.range.today.active {
color: #000;
background-color: #dcb369;
border-color: #d08d14;
color: #212529;
background-color: #dfb76d;
border-color: #d49219;
}
.datepicker table tr td.range.today:active:hover, .datepicker table tr td.range.today:active:focus, .datepicker table tr td.range.today.focus:active, .datepicker table tr td.range.today.active:hover, .datepicker table tr td.range.today.active:focus, .datepicker table tr td.range.today.active.focus {
color: #000;
background-color: #cba561;
border-color: #b17811;
.datepicker table tr td.range.today:active:hover, .datepicker table tr td.range.today:active:focus, .datepicker table tr td.range.today:active.focus, .datepicker table tr td.range.today.active:hover, .datepicker table tr td.range.today.active:focus, .datepicker table tr td.range.today.active.focus {
color: #212529;
background-color: #d0ab68;
border-color: #ba821b;
}
.datepicker table tr td.range.today.disabled:hover, .datepicker table tr td.range.today.disabled:focus, .datepicker table tr td.range.today.disabled.focus, .datepicker table tr td.range.today[disabled]:hover, .datepicker table tr td.range.today[disabled]:focus, .datepicker table tr td.range.today.focus[disabled], fieldset[disabled] .datepicker table tr td.range.today:hover, fieldset[disabled] .datepicker table tr td.range.today:focus, fieldset[disabled] .datepicker table tr td.range.today.focus {
.datepicker table tr td.range.today.disabled:hover, .datepicker table tr td.range.today.disabled:focus, .datepicker table tr td.range.today.disabled.focus, .datepicker table tr td.range.today[disabled]:hover, .datepicker table tr td.range.today[disabled]:focus, .datepicker table tr td.range.today[disabled].focus,
fieldset[disabled] .datepicker table tr td.range.today:hover,
fieldset[disabled] .datepicker table tr td.range.today:focus,
fieldset[disabled] .datepicker table tr td.range.today.focus {
background-color: #f4c775;
border-color: #eca117;
}
@@ -363,13 +378,19 @@
border-color: #7d7f82;
}
.datepicker table tr td.selected:active:hover, .datepicker table tr td.selected:active:focus, .datepicker table tr td.selected.focus:active, .datepicker table tr td.selected.active:hover, .datepicker table tr td.selected.active:focus, .datepicker table tr td.selected.active.focus, .datepicker table tr td.selected.highlighted:active:hover, .datepicker table tr td.selected.highlighted:active:focus, .datepicker table tr td.selected.highlighted.focus:active, .datepicker table tr td.selected.highlighted.active:hover, .datepicker table tr td.selected.highlighted.active:focus, .datepicker table tr td.selected.highlighted.active.focus {
.datepicker table tr td.selected:active:hover, .datepicker table tr td.selected:active:focus, .datepicker table tr td.selected:active.focus, .datepicker table tr td.selected.active:hover, .datepicker table tr td.selected.active:focus, .datepicker table tr td.selected.active.focus, .datepicker table tr td.selected.highlighted:active:hover, .datepicker table tr td.selected.highlighted:active:focus, .datepicker table tr td.selected.highlighted:active.focus, .datepicker table tr td.selected.highlighted.active:hover, .datepicker table tr td.selected.highlighted.active:focus, .datepicker table tr td.selected.highlighted.active.focus {
color: #fff;
background-color: #9d9fa0;
border-color: #909295;
}
.datepicker table tr td.selected.disabled:hover, .datepicker table tr td.selected.disabled:focus, .datepicker table tr td.selected.disabled.focus, .datepicker table tr td.selected[disabled]:hover, .datepicker table tr td.selected[disabled]:focus, .datepicker table tr td.selected.focus[disabled], fieldset[disabled] .datepicker table tr td.selected:hover, fieldset[disabled] .datepicker table tr td.selected:focus, fieldset[disabled] .datepicker table tr td.selected.focus, .datepicker table tr td.selected.highlighted.disabled:hover, .datepicker table tr td.selected.highlighted.disabled:focus, .datepicker table tr td.selected.highlighted.disabled.focus, .datepicker table tr td.selected.highlighted[disabled]:hover, .datepicker table tr td.selected.highlighted[disabled]:focus, .datepicker table tr td.selected.highlighted.focus[disabled], fieldset[disabled] .datepicker table tr td.selected.highlighted:hover, fieldset[disabled] .datepicker table tr td.selected.highlighted:focus, fieldset[disabled] .datepicker table tr td.selected.highlighted.focus {
.datepicker table tr td.selected.disabled:hover, .datepicker table tr td.selected.disabled:focus, .datepicker table tr td.selected.disabled.focus, .datepicker table tr td.selected[disabled]:hover, .datepicker table tr td.selected[disabled]:focus, .datepicker table tr td.selected[disabled].focus,
fieldset[disabled] .datepicker table tr td.selected:hover,
fieldset[disabled] .datepicker table tr td.selected:focus,
fieldset[disabled] .datepicker table tr td.selected.focus, .datepicker table tr td.selected.highlighted.disabled:hover, .datepicker table tr td.selected.highlighted.disabled:focus, .datepicker table tr td.selected.highlighted.disabled.focus, .datepicker table tr td.selected.highlighted[disabled]:hover, .datepicker table tr td.selected.highlighted[disabled]:focus, .datepicker table tr td.selected.highlighted[disabled].focus,
fieldset[disabled] .datepicker table tr td.selected.highlighted:hover,
fieldset[disabled] .datepicker table tr td.selected.highlighted:focus,
fieldset[disabled] .datepicker table tr td.selected.highlighted.focus {
background-color: #898b8d;
border-color: #6b6e71;
}
@@ -399,13 +420,19 @@
border-color: #2087f5;
}
.datepicker table tr td.active:active:hover, .datepicker table tr td.active:active:focus, .datepicker table tr td.active.focus:active, .datepicker table tr td.active.active:hover, .datepicker table tr td.active.active:focus, .datepicker table tr td.active.active.focus, .datepicker table tr td.active.highlighted:active:hover, .datepicker table tr td.active.highlighted:active:focus, .datepicker table tr td.active.highlighted.focus:active, .datepicker table tr td.active.highlighted.active:hover, .datepicker table tr td.active.highlighted.active:focus, .datepicker table tr td.active.highlighted.active.focus {
.datepicker table tr td.active:active:hover, .datepicker table tr td.active:active:focus, .datepicker table tr td.active:active.focus, .datepicker table tr td.active.active:hover, .datepicker table tr td.active.active:focus, .datepicker table tr td.active.active.focus, .datepicker table tr td.active.highlighted:active:hover, .datepicker table tr td.active.highlighted:active:focus, .datepicker table tr td.active.highlighted:active.focus, .datepicker table tr td.active.highlighted.active:hover, .datepicker table tr td.active.highlighted.active:focus, .datepicker table tr td.active.highlighted.active.focus {
color: #fff;
background-color: #2b91ff;
border-color: #4199f7;
}
.datepicker table tr td.active.disabled:hover, .datepicker table tr td.active.disabled:focus, .datepicker table tr td.active.disabled.focus, .datepicker table tr td.active[disabled]:hover, .datepicker table tr td.active[disabled]:focus, .datepicker table tr td.active.focus[disabled], fieldset[disabled] .datepicker table tr td.active:hover, fieldset[disabled] .datepicker table tr td.active:focus, fieldset[disabled] .datepicker table tr td.active.focus, .datepicker table tr td.active.highlighted.disabled:hover, .datepicker table tr td.active.highlighted.disabled:focus, .datepicker table tr td.active.highlighted.disabled.focus, .datepicker table tr td.active.highlighted[disabled]:hover, .datepicker table tr td.active.highlighted[disabled]:focus, .datepicker table tr td.active.highlighted.focus[disabled], fieldset[disabled] .datepicker table tr td.active.highlighted:hover, fieldset[disabled] .datepicker table tr td.active.highlighted:focus, fieldset[disabled] .datepicker table tr td.active.highlighted.focus {
.datepicker table tr td.active.disabled:hover, .datepicker table tr td.active.disabled:focus, .datepicker table tr td.active.disabled.focus, .datepicker table tr td.active[disabled]:hover, .datepicker table tr td.active[disabled]:focus, .datepicker table tr td.active[disabled].focus,
fieldset[disabled] .datepicker table tr td.active:hover,
fieldset[disabled] .datepicker table tr td.active:focus,
fieldset[disabled] .datepicker table tr td.active.focus, .datepicker table tr td.active.highlighted.disabled:hover, .datepicker table tr td.active.highlighted.disabled:focus, .datepicker table tr td.active.highlighted.disabled.focus, .datepicker table tr td.active.highlighted[disabled]:hover, .datepicker table tr td.active.highlighted[disabled]:focus, .datepicker table tr td.active.highlighted[disabled].focus,
fieldset[disabled] .datepicker table tr td.active.highlighted:hover,
fieldset[disabled] .datepicker table tr td.active.highlighted:focus,
fieldset[disabled] .datepicker table tr td.active.highlighted.focus {
background-color: #007bff;
border-color: #0277f4;
}
@@ -422,7 +449,7 @@
}
.datepicker table tr td span:hover, .datepicker table tr td span.focused {
color: #000;
color: #212529;
background: #e9e9ea;
}
@@ -439,7 +466,7 @@
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.datepicker table tr td span.active:focus, .datepicker table tr td span.active.focus, .datepicker table tr td span.active:hover:focus, .datepicker table tr td span.active.focus:hover, .datepicker table tr td span.active.disabled:focus, .datepicker table tr td span.active.disabled.focus, .datepicker table tr td span.active.disabled:hover:focus, .datepicker table tr td span.active.disabled.focus:hover {
.datepicker table tr td span.active:focus, .datepicker table tr td span.active.focus, .datepicker table tr td span.active:hover:focus, .datepicker table tr td span.active:hover.focus, .datepicker table tr td span.active.disabled:focus, .datepicker table tr td span.active.disabled.focus, .datepicker table tr td span.active.disabled:hover:focus, .datepicker table tr td span.active.disabled:hover.focus {
color: #fff;
background-color: #1a88ff;
border-color: #4199f7;
@@ -451,19 +478,31 @@
border-color: #2087f5;
}
.datepicker table tr td span.active:active, .datepicker table tr td span.active.active, .datepicker table tr td span.active:hover:active, .datepicker table tr td span.active.active:hover, .datepicker table tr td span.active.disabled:active, .datepicker table tr td span.active.disabled.active, .datepicker table tr td span.active.disabled:hover:active, .datepicker table tr td span.active.disabled.active:hover {
.datepicker table tr td span.active:active, .datepicker table tr td span.active.active, .datepicker table tr td span.active:hover:active, .datepicker table tr td span.active:hover.active, .datepicker table tr td span.active.disabled:active, .datepicker table tr td span.active.disabled.active, .datepicker table tr td span.active.disabled:hover:active, .datepicker table tr td span.active.disabled:hover.active {
color: #fff;
background-color: #1a88ff;
border-color: #2087f5;
}
.datepicker table tr td span.active:active:hover, .datepicker table tr td span.active:active:focus, .datepicker table tr td span.active.focus:active, .datepicker table tr td span.active.active:hover, .datepicker table tr td span.active.active:focus, .datepicker table tr td span.active.active.focus, .datepicker table tr td span.active:hover:active:hover, .datepicker table tr td span.active:hover:active:focus, .datepicker table tr td span.active.focus:hover:active, .datepicker table tr td span.active.active:hover:hover, .datepicker table tr td span.active.active:hover:focus, .datepicker table tr td span.active.active.focus:hover, .datepicker table tr td span.active.disabled:active:hover, .datepicker table tr td span.active.disabled:active:focus, .datepicker table tr td span.active.disabled.focus:active, .datepicker table tr td span.active.disabled.active:hover, .datepicker table tr td span.active.disabled.active:focus, .datepicker table tr td span.active.disabled.active.focus, .datepicker table tr td span.active.disabled:hover:active:hover, .datepicker table tr td span.active.disabled:hover:active:focus, .datepicker table tr td span.active.disabled.focus:hover:active, .datepicker table tr td span.active.disabled.active:hover:hover, .datepicker table tr td span.active.disabled.active:hover:focus, .datepicker table tr td span.active.disabled.active.focus:hover {
.datepicker table tr td span.active:active:hover, .datepicker table tr td span.active:active:focus, .datepicker table tr td span.active:active.focus, .datepicker table tr td span.active.active:hover, .datepicker table tr td span.active.active:focus, .datepicker table tr td span.active.active.focus, .datepicker table tr td span.active:hover:active:hover, .datepicker table tr td span.active:hover:active:focus, .datepicker table tr td span.active:hover:active.focus, .datepicker table tr td span.active:hover.active:hover, .datepicker table tr td span.active:hover.active:focus, .datepicker table tr td span.active:hover.active.focus, .datepicker table tr td span.active.disabled:active:hover, .datepicker table tr td span.active.disabled:active:focus, .datepicker table tr td span.active.disabled:active.focus, .datepicker table tr td span.active.disabled.active:hover, .datepicker table tr td span.active.disabled.active:focus, .datepicker table tr td span.active.disabled.active.focus, .datepicker table tr td span.active.disabled:hover:active:hover, .datepicker table tr td span.active.disabled:hover:active:focus, .datepicker table tr td span.active.disabled:hover:active.focus, .datepicker table tr td span.active.disabled:hover.active:hover, .datepicker table tr td span.active.disabled:hover.active:focus, .datepicker table tr td span.active.disabled:hover.active.focus {
color: #fff;
background-color: #2b91ff;
border-color: #4199f7;
}
.datepicker table tr td span.active.disabled:hover, .datepicker table tr td span.active.disabled:focus, .datepicker table tr td span.active.disabled.focus, .datepicker table tr td span.active[disabled]:hover, .datepicker table tr td span.active[disabled]:focus, .datepicker table tr td span.active.focus[disabled], fieldset[disabled] .datepicker table tr td span.active:hover, fieldset[disabled] .datepicker table tr td span.active:focus, fieldset[disabled] .datepicker table tr td span.active.focus, .datepicker table tr td span.active.disabled:hover:hover, .datepicker table tr td span.active.disabled:hover:focus, .datepicker table tr td span.active.disabled.focus:hover, .datepicker table tr td span.active[disabled]:hover:hover, .datepicker table tr td span.active[disabled]:hover:focus, .datepicker table tr td span.active.focus[disabled]:hover, fieldset[disabled] .datepicker table tr td span.active:hover:hover, fieldset[disabled] .datepicker table tr td span.active:hover:focus, fieldset[disabled] .datepicker table tr td span.active.focus:hover, .datepicker table tr td span.active.disabled.disabled:hover, .datepicker table tr td span.active.disabled.disabled:focus, .datepicker table tr td span.active.disabled.disabled.focus, .datepicker table tr td span.active.disabled[disabled]:hover, .datepicker table tr td span.active.disabled[disabled]:focus, .datepicker table tr td span.active.disabled.focus[disabled], fieldset[disabled] .datepicker table tr td span.active.disabled:hover, fieldset[disabled] .datepicker table tr td span.active.disabled:focus, fieldset[disabled] .datepicker table tr td span.active.disabled.focus, .datepicker table tr td span.active.disabled.disabled:hover:hover, .datepicker table tr td span.active.disabled.disabled:hover:focus, .datepicker table tr td span.active.disabled.disabled.focus:hover, .datepicker table tr td span.active.disabled[disabled]:hover:hover, .datepicker table tr td span.active.disabled[disabled]:hover:focus, .datepicker table tr td span.active.disabled.focus[disabled]:hover, fieldset[disabled] .datepicker table tr td span.active.disabled:hover:hover, fieldset[disabled] .datepicker table tr td span.active.disabled:hover:focus, fieldset[disabled] .datepicker table tr td span.active.disabled.focus:hover {
.datepicker table tr td span.active.disabled:hover, .datepicker table tr td span.active.disabled:focus, .datepicker table tr td span.active.disabled.focus, .datepicker table tr td span.active[disabled]:hover, .datepicker table tr td span.active[disabled]:focus, .datepicker table tr td span.active[disabled].focus,
fieldset[disabled] .datepicker table tr td span.active:hover,
fieldset[disabled] .datepicker table tr td span.active:focus,
fieldset[disabled] .datepicker table tr td span.active.focus, .datepicker table tr td span.active:hover.disabled:hover, .datepicker table tr td span.active:hover.disabled:focus, .datepicker table tr td span.active:hover.disabled.focus, .datepicker table tr td span.active:hover[disabled]:hover, .datepicker table tr td span.active:hover[disabled]:focus, .datepicker table tr td span.active:hover[disabled].focus,
fieldset[disabled] .datepicker table tr td span.active:hover:hover,
fieldset[disabled] .datepicker table tr td span.active:hover:focus,
fieldset[disabled] .datepicker table tr td span.active:hover.focus, .datepicker table tr td span.active.disabled.disabled:hover, .datepicker table tr td span.active.disabled.disabled:focus, .datepicker table tr td span.active.disabled.disabled.focus, .datepicker table tr td span.active.disabled[disabled]:hover, .datepicker table tr td span.active.disabled[disabled]:focus, .datepicker table tr td span.active.disabled[disabled].focus,
fieldset[disabled] .datepicker table tr td span.active.disabled:hover,
fieldset[disabled] .datepicker table tr td span.active.disabled:focus,
fieldset[disabled] .datepicker table tr td span.active.disabled.focus, .datepicker table tr td span.active.disabled:hover.disabled:hover, .datepicker table tr td span.active.disabled:hover.disabled:focus, .datepicker table tr td span.active.disabled:hover.disabled.focus, .datepicker table tr td span.active.disabled:hover[disabled]:hover, .datepicker table tr td span.active.disabled:hover[disabled]:focus, .datepicker table tr td span.active.disabled:hover[disabled].focus,
fieldset[disabled] .datepicker table tr td span.active.disabled:hover:hover,
fieldset[disabled] .datepicker table tr td span.active.disabled:hover:focus,
fieldset[disabled] .datepicker table tr td span.active.disabled:hover.focus {
background-color: #007bff;
border-color: #0277f4;
}
@@ -487,7 +526,7 @@
.datepicker .prev:hover,
.datepicker .next:hover,
.datepicker tfoot tr th:hover {
color: #000;
color: #212529;
background: #e9e9ea;
}
@@ -531,3 +570,4 @@
margin-left: -5px;
margin-right: -5px;
}

File diff suppressed because one or more lines are too long

View File

@@ -7,7 +7,7 @@
// Variables and mixins copied from Bootstrap 3.3.5
// These are BS3 variables that are used in datepicker3.scss. So, when compiling against
// a BS3 bslib theme, these variables should already be defined. Here we set
// a BS3 bootstraplib theme, these variables should already be defined. Here we set
// *defaults* for these variables based on BS4 variables, so this scss can work for
// both BS3 and BS4
$gray: mix($body-bg, $body-color, 33.5%) !default;
@@ -26,12 +26,27 @@ $dropdown-border: $dropdown-border-color !default;
//$btn-link-disabled-color: $gray-light;
//$dropdown-bg: #fff;
// Setup BS4 style color contrasting
$yiq-contrasted-threshold: 150 !default;
$yiq-text-dark: #212529 !default;
$yiq-text-light: #fff !default;
@function color-yiq($color, $dark: $yiq-text-dark, $light: $yiq-text-light) {
$r: red($color);
$g: green($color);
$b: blue($color);
$yiq: (($r * 299) + ($g * 587) + ($b * 114)) / 1000;
@if ($yiq >= $yiq-contrasted-threshold) {
@return $dark;
} @else {
@return $light;
}
}
@mixin button-variant($background, $border) {
$color: color-contrast($background);
$color: color-yiq($background);
color: $color;
background-color: $background;

View File

@@ -1,15 +1,5 @@
// Both BS3 and BS4 define a border radius mixin, but just in case
// we're trying to compile this without bootstrapSass
@mixin border-radius-shim($radius) {
@if mixin-exists("border-radius") {
@include border-radius($radius);
} @else {
border-radius: $radius;
}
}
.datepicker {
@include border-radius-shim($border-radius-base);
border-radius: $border-radius-base;
&-inline {
width: 220px;
}
@@ -74,7 +64,7 @@
text-align: center;
width: 30px;
height: 30px;
@include border-radius-shim(4px);
border-radius: 4px;
border: none;
}
}
@@ -93,7 +83,7 @@
}
&.day:hover,
&.focused {
color: color-contrast($gray-lighter);
color: color-yiq($gray-lighter);
background: $gray-lighter;
cursor: pointer;
}
@@ -189,10 +179,10 @@
float: left;
margin: 1%;
cursor: pointer;
@include border-radius-shim(4px);
border-radius: 4px;
&:hover,
&.focused {
color: color-contrast($gray-lighter);
color: color-yiq($gray-lighter);
background: $gray-lighter;
}
&.disabled,
@@ -225,7 +215,7 @@
tfoot tr th {
cursor: pointer;
&:hover {
color: color-contrast($gray-lighter);
color: color-yiq($gray-lighter);
background: $gray-lighter;
}
}
@@ -253,10 +243,10 @@
text-align: center;
}
input:first-child {
@include border-radius-shim(3px 0 0 3px);
border-radius: 3px 0 0 3px;
}
input:last-child {
@include border-radius-shim(0 3px 3px 0);
border-radius: 0 3px 3px 0;
}
.input-group-addon {
width: auto;

View File

@@ -261,3 +261,4 @@
.irs--shiny .irs-grid-pol.small {
background-color: #999999;
}

View File

@@ -6,17 +6,25 @@
@import "_base";
// Both BS3 and BS4 define a border radius mixin, but just in case
// we're trying to compile this without bootstrapSass
@mixin border-radius-shim($radius) {
@if mixin-exists("border-radius") {
@include border-radius($radius);
////////////////////////////////////////////////////////////////////////////
// Setup BS4 style color contrasting
$yiq-contrasted-threshold: 150 !default;
$yiq-text-dark: #212529 !default;
$yiq-text-light: #fff !default;
@function color-yiq($color, $dark: $yiq-text-dark, $light: $yiq-text-light) {
$r: red($color);
$g: green($color);
$b: blue($color);
$yiq: (($r * 299) + ($g * 587) + ($b * 114)) / 1000;
@if ($yiq >= $yiq-contrasted-threshold) {
@return $dark;
} @else {
border-radius: $radius;
@return $light;
}
}
////////////////////////////////////////////////////////////////////////////
// Re-define font-family on .irs to make it configurable
@@ -58,7 +66,7 @@ $font-family: Arial, sans-serif !default;
$minmax_line_height: 1.333 !default;
$fromto_bg_color: $accent !default;
$fromto_color: color-contrast($fromto_bg_color) !default;
$fromto_color: color-yiq($fromto_bg_color) !default;
$fromto_font_size: 11px !default;
$fromto_line_height: 1.333 !default;
@@ -79,7 +87,7 @@ $font-family: Arial, sans-serif !default;
background: $line_bg;
background-color: $line_bg_color;
border: $line_border;
@include border-radius-shim($line_height);
border-radius: $line_height;
}
.#{$name}-bar {
@@ -90,7 +98,7 @@ $font-family: Arial, sans-serif !default;
background: $bar_color;
&--single {
@include border-radius-shim($line_height 0 0 $line_height);
border-radius: $line_height 0 0 $line_height;
}
}
@@ -99,7 +107,7 @@ $font-family: Arial, sans-serif !default;
top: 38px;
height: 2px;
background: rgba($fg, 0.3);
@include border-radius-shim(5px);
border-radius: 5px;
}
.lt-ie9 .#{$name}-shadow {
filter: alpha(opacity=30);
@@ -114,7 +122,7 @@ $font-family: Arial, sans-serif !default;
border: $handle_border;
background-color: $handle_color;
box-shadow: $handle_box_shadow;
@include border-radius-shim($handle_width);
border-radius: $handle_width;
&.state_hover,
&:hover {
@@ -129,7 +137,7 @@ $font-family: Arial, sans-serif !default;
color: $minmax_text_color;
text-shadow: none;
background-color: $minmax_bg_color;
@include border-radius-shim($custom_radius);
border-radius: $custom_radius;
font-size: $minmax_font_size;
line-height: $minmax_line_height;
}
@@ -146,7 +154,7 @@ $font-family: Arial, sans-serif !default;
text-shadow: none;
padding: 1px 3px;
background-color: $fromto_bg_color;
@include border-radius-shim($custom_radius);
border-radius: $custom_radius;
font-size: $fromto_font_size;
line-height: $fromto_line_height;
}

View File

@@ -57,11 +57,9 @@ $selectize-caret-margin: 0 2px 0 0 !default;
$selectize-caret-margin-rtl: 0 4px 0 -2px !default;
@mixin selectize-border-radius($radii){
@if mixin-exists("border-radius") {
@include border-radius($radii)
} @else {
border-radius: $radii;
}
-webkit-border-radius: $radii;
-moz-border-radius: $radii;
border-radius: $radii;
}
@mixin selectize-unselectable(){
-webkit-user-select: none;

View File

@@ -9,7 +9,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
(function () {
var $ = jQuery;
var exports = window.Shiny = window.Shiny || {};
exports.version = "1.5.0.9005"; // Version number inserted by Grunt
exports.version = "1.5.0.9004"; // Version number inserted by Grunt
var origPushState = window.history.pushState;
@@ -162,7 +162,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
function pixelRatio() {
if (window.devicePixelRatio) {
return Math.round(window.devicePixelRatio * 100) / 100;
return window.devicePixelRatio;
} else {
return 1;
}
@@ -364,26 +364,18 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
} // Detect IE information
var ua = window.navigator.userAgent;
var isIE = /MSIE|Trident/.test(ua);
var isIE = navigator.appName === 'Microsoft Internet Explorer';
function getIEVersion() {
var msie = ua.indexOf('MSIE ');
var rv = -1;
if (isIE && msie > 0) {
// IE 10 or older => return version number
return parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10);
if (isIE) {
var ua = navigator.userAgent;
var re = new RegExp("MSIE ([0-9]{1,}[\\.0-9]{0,})");
if (re.exec(ua) !== null) rv = parseFloat(RegExp.$1);
}
var trident = ua.indexOf('Trident/');
if (trident > 0) {
// IE 11 => return version number
var rv = ua.indexOf('rv:');
return parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10);
}
return -1;
return rv;
}
return {
@@ -3961,86 +3953,26 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
}
if (dep.stylesheet) {
var links = $.map(asArray(dep.stylesheet), function (stylesheet) {
return $("<link rel='stylesheet' type='text/css'>").attr("href", href + "/" + encodeURI(stylesheet));
var stylesheets = $.map(asArray(dep.stylesheet), function (stylesheet) {
var this_href = href + "/" + encodeURI(stylesheet); // If a styleSheet with this href is already active, then disable it
var re = new RegExp(this_href + "$");
for (var i = 0; i < document.styleSheets.length; i++) {
var h = document.styleSheets[i].href;
if (!h) continue;
if (restyle && h.match(re)) document.styleSheets[i].disabled = true;
} // Force client to re-request the stylesheet if we've already seen it
// (without this unique param, the request might get cached)
if (restyle) {
this_href = this_href + "?restyle=" + new Date().getTime();
}
return $("<link rel='stylesheet' type='text/css'>").attr("href", this_href);
});
if (!restyle) {
$head.append(links);
} else {
// This inline <style> based approach works for IE11
var refreshStyle = function refreshStyle(href, oldSheet) {
var xhr = new XMLHttpRequest();
xhr.open('GET', href);
xhr.onload = function () {
var id = "shiny_restyle_" + href.split("?restyle")[0].replace(/\W/g, '_');
var oldStyle = $head.find("style#" + id);
var newStyle = $("<style>").attr("id", id).html(xhr.responseText);
$head.append(newStyle);
setTimeout(function () {
return oldStyle.remove();
}, 500);
setTimeout(function () {
return removeSheet(oldSheet);
}, 500);
};
xhr.send();
};
var findSheet = function findSheet(href) {
for (var i = 0; i < document.styleSheets.length; i++) {
var sheet = document.styleSheets[i]; // The sheet's href is a full URL
if (typeof sheet.href === "string" && sheet.href.indexOf(href) > -1) {
return sheet;
}
}
return null;
};
var removeSheet = function removeSheet(sheet) {
if (!sheet) return;
sheet.disabled = true;
if (browser.isIE) sheet.cssText = "";
$(sheet.ownerNode).remove();
};
$.map(links, function (link) {
// Find any document.styleSheets that match this link's href
// so we can remove it after bringing in the new stylesheet
var oldSheet = findSheet(link.attr("href")); // Add a timestamp to the href to prevent caching
var href = link.attr("href") + "?restyle=" + new Date().getTime(); // Use inline <style> approach for IE, otherwise use the more elegant
// <link> -based approach
if (browser.isIE) {
refreshStyle(href, oldSheet);
} else {
link.attr("href", href); // Once the new <link> is loaded, schedule the old <link> to be removed
// on the next tick which is needed to avoid FOUC
link.attr("onload", function () {
setTimeout(function () {
return removeSheet(oldSheet);
}, 500);
});
$head.append(link);
}
}); // Once the new styles are applied, CSS values that are accessible server-side
// (e.g., getCurrentOutputInfo(), output visibility, etc) may become outdated.
// At the time of writing, that means we need to do sendImageSize() &
// sendOutputHiddenState() again, which can be done by re-binding.
/* global Shiny */
var bindDebouncer = new Debouncer(null, Shiny.bindAll, 100);
setTimeout(function () {
return bindDebouncer.normalCall();
}, 100);
}
$head.append(stylesheets);
}
if (dep.script && !restyle) {

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

File diff suppressed because one or more lines are too long

View File

@@ -18,10 +18,8 @@ $shiny-table-na: mix($bg, $fg, 54%) !default;
$shiny-error-validated-color: mix($bg, $fg, 50%) !default;
// TODO: what should this be? mix of primary and bg?
$shiny-progress-text-bg-color: #eef8ff !default;
$shiny-input-panel-bg: mix($bg, $fg, 96%) !default;
$shiny-input-panel-bg: mix($bg, $fg, 95%) !default;
$shiny-input-panel-border: $border-width solid $border-color !default;
$shiny-input-panel-border-radius: $border-radius !default;
$shiny-text-output-border-radius: $border-radius !default;
$notification-bg-color: mix($bg, $fg, 90%) !default;
$notification-color: $fg !default;
$notification-border: $border-width solid $border-color !default;

View File

@@ -7,8 +7,6 @@ $shiny-progress-text-bg-color: #eef8ff !default; // TODO: what is this actu
$shiny-input-width: 300px !default;
$shiny-input-panel-bg: #f5f5f5 !default;
$shiny-input-panel-border: 1px solid #e3e3e3 !default;
$shiny-input-panel-border-radius: 2px !default;
$shiny-text-output-border-radius: null !default;
$notification-bg-color: #e8e8e8 !default;
$notification-color: #333 !default;
$notification-border: 1px solid #ccc !default;
@@ -44,16 +42,6 @@ $shiny-file-over-shadow: inset 0 1px 1px rgba(black, .075), 0 0 8px r
}
}
// Both BS3 and BS4 define a border radius mixin, but just in case
// we're trying to compile this without bootstrapSass
@mixin border-radius-shim($radius) {
@if mixin-exists("border-radius") {
@include border-radius($radius);
} @else {
border-radius: $radius;
}
}
// RULES
@@ -76,7 +64,6 @@ pre.shiny-text-output.noplaceholder:empty {
*/
pre.shiny-text-output {
word-wrap: normal;
@include border-radius-shim($shiny-text-output-border-radius);
}
.shiny-image-output, .shiny-plot-output {
@@ -329,7 +316,7 @@ pre.shiny-text-output {
margin-bottom: 6px;
background-color: $shiny-input-panel-bg;
border: $shiny-input-panel-border;
@include border-radius-shim($shiny-input-panel-border-radius);
border-radius: 2px;
}
/* For checkbox groups and radio buttons, bring the options closer to label,
@@ -377,7 +364,7 @@ pre.shiny-text-output {
background-color: $notification-bg-color;
color: $notification-color;
border: $notification-border;
@include border-radius-shim($notification-border-radius);
border-radius: $notification-border-radius;
opacity: 0.85;
padding: $notification-padding;
margin: 2px;

View File

@@ -50,9 +50,9 @@ session$setInputs(x=1, y=2)
\item{\code{token}}{On a real \code{ShinySession}, used to identify this instance in URLs.}
\item{\code{cache}}{The session cache object.}
\item{\code{cache}}{The session cache MemoryCache.}
\item{\code{appcache}}{The app cache object.}
\item{\code{appcache}}{The app cache MemoryCache.}
\item{\code{restoreContext}}{Part of bookmarking support in a real
\code{ShinySession} but always \code{NULL} for a \code{MockShinySession}.}

View File

@@ -1,423 +0,0 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/bind-cache.R
\name{bindCache}
\alias{bindCache}
\title{Add caching with reactivity to an object}
\usage{
bindCache(x, ..., cache = "app")
}
\arguments{
\item{x}{The object to add caching to.}
\item{...}{One or more expressions to use in the caching key.}
\item{cache}{The scope of the cache, or a cache object. This can be \code{"app"}
(the default), \code{"session"}, or a cache object like a
\code{\link[cachem:cache_disk]{cachem::cache_disk()}}. See the Cache Scoping section for more information.}
}
\description{
\code{bindCache()} adds caching \code{\link[=reactive]{reactive()}} expressions and \verb{render*} functions
(like \code{\link[=renderText]{renderText()}}, \code{\link[=renderTable]{renderTable()}}, ...).
Ordinary \code{\link[=reactive]{reactive()}} expressions automatically cache their \emph{most recent}
value, which helps to avoid redundant computation in downstream reactives.
\code{bindCache()} will cache all previous values (as long as they fit in the
cache) and they can be shared across user sessions. This allows
\code{bindCache()} to dramatically improve performance when used correctly.
}
\details{
\code{bindCache()} requires one or more expressions that are used to generate a
\strong{cache key}, which is used to determine if a computation has occurred
before and hence can be retrieved from the cache. If you're familiar with the
concept of memoizing pure functions (e.g., the \pkg{memoise} package), you
can think of the cache key as the input(s) to a pure function. As such, one
should take care to make sure the use of \code{bindCache()} is \emph{pure} in the same
sense, namely:
\enumerate{
\item For a given key, the return value is always the same.
\item Evaluation has no side-effects.
}
In the example here, the \code{bindCache()} key consists of \code{input$x} and
\code{input$y} combined, and the value is \code{input$x * input$y}. In this simple
example, for any given key, there is only one possible returned value.\if{html}{\out{<div class="NA">}}\preformatted{r <- reactive(\{ input$x * input$y \}) \%>\%
bindCache(input$x, input$y)
}\if{html}{\out{</div>}}
The largest performance improvements occur when the cache key is fast to
compute and the reactive expression is slow to compute. To see if the value
should be computed, a cached reactive evaluates the key, and then serializes
and hashes the result. If the resulting hashed key is in the cache, then the
cached reactive simply retrieves the previously calculated value and returns
it; if not, then the value is computed and the result is stored in the cache
before being returned.
To compute the cache key, \code{bindCache()} hashes the contents of \code{...}, so it's
best to avoid including large objects in a cache key since that can result in
slow hashing. It's also best to avoid reference objects like environments and
R6 objects, since the serialization of these objects may not capture relevant
changes.
If you want to use a large object as part of a cache key, it may make sense
to do some sort of reduction on the data that still captures information
about whether a value can be retrieved from the cache. For example, if you
have a large data set with timestamps, it might make sense to extract the
most recent timestamp and return that. Then, instead of hashing the entire
data object, the cached reactive only needs to hash the timestamp.\if{html}{\out{<div class="NA">}}\preformatted{r <- reactive(\{ compute(bigdata()) \} \%>\%
bindCache(\{ extract_most_recent_time(bigdata()) \})
}\if{html}{\out{</div>}}
For computations that are vert slow, it often makes sense to pair
\code{\link[=bindCache]{bindCache()}} with \code{\link[=bindEvent]{bindEvent()}} so that no computation is performed until
the user explicitly requests it (for more, see the Details section of
\code{\link[=bindEvent]{bindEvent()}}).
}
\section{Cache keys and reactivity}{
Because the \strong{value} expression (from the original \code{\link[=reactive]{reactive()}}) is
cached, it is not necessarily re-executed when someone retrieves a value,
and therefore it can't be used to decide what objects to take reactive
dependencies on. Instead, the \strong{key} is used to figure out which objects
to take reactive dependencies on. In short, the key expression is reactive,
and value expression is no longer reactive.
Here's an example of what not to do: if the key is \code{input$x} and the value
expression is from \code{reactive({input$x + input$y})}, then the resulting
cached reactive will only take a reactive dependency on \code{input$x} -- it
won't recompute \code{{input$x + input$y}} when just \code{input$y} changes.
Moreover, the cache won't use \code{input$y} as part of the key, and so it could
return incorrect values in the future when it retrieves values from the
cache. (See the examples below for an example of this.)
A better cache key would be something like \verb{input$x, input$y}. This does
two things: it ensures that a reactive dependency is taken on both
\code{input$x} and \code{input$y}, and it also makes sure that both values are
represented in the cache key.
In general, \code{key} should use the same reactive inputs as \code{value}, but the
computation should be simpler. If there are other (non-reactive) values
that are consumed, such as external data sources, they should be used in
the \code{key} as well. Note that if the \code{key} is large, it can make sense to do
some sort of reduction on it so that the serialization and hashing of the
cache key is not too expensive.
Remember that the key is \emph{reactive}, so it is not re-executed every single
time that someone accesses the cached reactive. It is only re-executed if
it has been invalidated by one of the reactives it depends on. For
example, suppose we have this cached reactive:\if{html}{\out{<div class="NA">}}\preformatted{r <- reactive(\{ input$x * input$y \}) \%>\%
bindCache(input$x, input$y)
}\if{html}{\out{</div>}}
In this case, the key expression is essentially \code{reactive(list(input$x, input$y))} (there's a bit more to it, but that's a good enough
approximation). The first time \code{r()} is called, it executes the key, then
fails to find it in the cache, so it executes the value expression, \code{{ input$x + input$y }}. If \code{r()} is called again, then it does not need to
re-execute the key expression, because it has not been invalidated via a
change to \code{input$x} or \code{input$y}; it simply returns the previous value.
However, if \code{input$x} or \code{input$y} changes, then the reactive expression will
be invalidated, and the next time that someone calls \code{r()}, the key
expression will need to be re-executed.
Note that if the cached reactive is passed to \code{\link[=bindEvent]{bindEvent()}}, then the key
expression will no longer be reactive; instead, the event expression will be
reactive.
}
\section{Cache scope}{
By default, when \code{bindCache()} is used, it is scoped to the running
application. That means that it shares a cache with all user sessions
connected to the application (within the R process). This is done with the
\code{cache} parameter's default value, \code{"app"}.
With an app-level cache scope, one user can benefit from the work done for
another user's session. In most cases, this is the best way to get
performance improvements from caching. However, in some cases, this could
leak information between sessions. For example, if the cache key does not
fully encompass the inputs used by the value, then data could leak between
the sessions. Or if a user sees that a cached reactive returns its value
very quickly, they may be able to infer that someone else has already used
it with the same values.
It is also possible to scope the cache to the session, with
\code{cache="session"}. This removes the risk of information leaking between
sessions, but then one session cannot benefit from computations performed in
another session.
It is possible to pass in caching objects directly to
\code{bindCache()}. This can be useful if, for example, you want to use a
particular type of cache with specific cached reactives, or if you want to
use a \code{\link[cachem:cache_disk]{cachem::cache_disk()}} that is shared across multiple processes and
persists beyond the current R session.
To use different settings for an application-scoped cache, you can call
\code{\link[=shinyOptions]{shinyOptions()}} at the top of your app.R, server.R, or
global.R. For example, this will create a cache with 500 MB of space
instead of the default 200 MB:\preformatted{shinyOptions(cache = cachem::cache_mem(size = 500e6))
}
To use different settings for a session-scoped cache, you can set
\code{self$cache} at the top of your server function. By default, it will create
a 200 MB memory cache for each session, but you can replace it with
something different. To use the session-scoped cache, you must also call
\code{bindCache()} with \code{cache="session"}. This will create a 100 MB cache for
the session:\preformatted{function(input, output, session) \{
session$cache <- cachem::cache_mem(size = 100e6)
...
\}
}
If you want to use a cache that is shared across multiple R processes, you
can use a \code{\link[cachem:cache_disk]{cachem::cache_disk()}}. You can create a application-level shared
cache by putting this at the top of your app.R, server.R, or global.R:\preformatted{shinyOptions(cache = cachem::cache_disk(file.path(dirname(tempdir()), "myapp-cache"))
}
This will create a subdirectory in your system temp directory named
\code{myapp-cache} (replace \code{myapp-cache} with a unique name of
your choosing). On most platforms, this directory will be removed when
your system reboots. This cache will persist across multiple starts and
stops of the R process, as long as you do not reboot.
To have the cache persist even across multiple reboots, you can create the
cache in a location outside of the temp directory. For example, it could
be a subdirectory of the application:\preformatted{shinyOptions(cache = cachem::cache_disk("./myapp-cache"))
}
In this case, resetting the cache will have to be done manually, by deleting
the directory.
You can also scope a cache to just one item, or selected items. To do that,
create a \code{\link[cachem:cache_mem]{cachem::cache_mem()}} or \code{\link[cachem:cache_disk]{cachem::cache_disk()}}, and pass it
as the \code{cache} argument of \code{bindCache()}.
}
\section{Computing cache keys}{
The actual cache key that is used internally takes value from evaluating
the key expression(s) (from the \code{...} arguments) and combines it with the
(unevaluated) value expression.
This means that if there are two cached reactives which have the same
result from evaluating the key, but different value expressions, then they
will not need to worry about collisions.
However, if two cached reactives have identical key and value expressions
expressions, they will share the cached values. This is useful when using
\code{cache="app"}: there may be multiple user sessions which create separate
cached reactive objects (because they are created from the same code in the
server function, but the server function is executed once for each user
session), and those cached reactive objects across sessions can share
values in the cache.
}
\section{Async with cached reactives}{
With a cached reactive expression, the key and/or value expression can be
\emph{asynchronous}. In other words, they can be promises --- not regular R
promises, but rather objects provided by the
\href{https://rstudio.github.io/promises/}{\pkg{promises}} package, which
are similar to promises in JavaScript. (See \code{\link[promises:promise]{promises::promise()}} for more
information.) You can also use \code{\link[future:future]{future::future()}} objects to run code in a
separate process or even on a remote machine.
If the value returns a promise, then anything that consumes the cached
reactive must expect it to return a promise.
Similarly, if the key is a promise (in other words, if it is asynchronous),
then the entire cached reactive must be asynchronous, since the key must be
computed asynchronously before it knows whether to compute the value or the
value is retrieved from the cache. Anything that consumes the cached
reactive must therefore expect it to return a promise.
}
\section{Developing render functions for caching}{
If you've implemented your own \verb{render*()} function, you may need to
provide a \code{cacheHint} to \code{\link[=createRenderFunction]{createRenderFunction()}} (or
\code{\link[htmlwidgets:htmlwidgets-shiny]{htmlwidgets::shinyRenderWidget()}}, if you've authored an htmlwidget) in
order for \code{bindCache()} to correctly compute a cache key.
The potential problem is a cache collision. Consider the following:\preformatted{output$x1 <- renderText(\{ input$x \}) \%>\% bindCache(input$x)
output$x2 <- renderText(\{ input$x * 2 \}) \%>\% bindCache(input$x)
}
Both \code{output$x1} and \code{output$x2} use \code{input$x} as part of their cache key,
but if it were the only thing used in the cache key, then the two outputs
would have a cache collision, and they would have the same output. To avoid
this, a \emph{cache hint} is automatically added when \code{\link[=renderText]{renderText()}} calls
\code{\link[=createRenderFunction]{createRenderFunction()}}. The cache hint is used as part of the actual
cache key, in addition to the one passed to \code{bindCache()} by the user. The
cache hint can be viewed by calling the internal Shiny function
\code{extractCacheHint()}:\if{html}{\out{<div class="NA">}}\preformatted{r <- renderText(\{ input$x \})
shiny:::extractCacheHint(r)
}\if{html}{\out{</div>}}
This returns a nested list containing an item, \verb{$origUserFunc$body}, which
in this case is the expression which was passed to \code{renderText()}:
\code{{ input$x }}. This (quoted) expression is mixed into the actual cache
key, and it is how \code{output$x1} does not have collisions with \code{output$x2}.
For most developers of render functions, nothing extra needs to be done;
the automatic inference of the cache hint is sufficient. Again, you can
check it by calling \code{shiny:::extractCacheHint()}, and by testing the
render function for cache collisions in a real application.
In some cases, however, the automatic cache hint inference is not
sufficient, and it is necessary to provide a cache hint. This is true
for \code{renderPrint()}. Unlike \code{renderText()}, it wraps the user-provided
expression in another function, before passing it to \code{\link[=markRenderFunction]{markRenderFunction()}}
(instead of \code{\link[=createRenderFunction]{createRenderFunction()}}). Because the user code is wrapped in
another function, markRenderFunction() is not able to automatically extract
the user-provided code and use it in the cache key. Instead, \code{renderPrint}
calls \code{markRenderFunction()}, it explicitly passes along a \code{cacheHint},
which includes a label and the original user expression.
In general, if you need to provide a \code{cacheHint}, it is best practice to
provide a \code{label} id, the user's \code{expr}, as well as any other arguments
that may influence the final value.
For \pkg{htmlwidgets}, it will try to automatically infer a cache hint;
again, you can inspect the cache hint with \code{shiny:::extractCacheHint()} and
also test it in an application. If you do need to explicitly provide a
cache hint, pass it to \code{shinyRenderWidget}. For example:\preformatted{renderMyWidget <- function(expr) \{
expr <- substitute(expr)
htmlwidgets::shinyRenderWidget(expr,
myWidgetOutput,
quoted = TRUE,
env = parent.frame(),
cacheHint = list(label = "myWidget", userExpr = expr)
)
\}
}
}
\section{Uncacheable objects}{
Some render functions cannot be cached, typically because they have side
effects or modify some external state, and they must re-execute each time
in order to work properly.
For developers of such code, they should call \code{\link[=createRenderFunction]{createRenderFunction()}} or
\code{\link[=markRenderFunction]{markRenderFunction()}} with \code{cacheHint = FALSE}.
}
\section{Caching with \code{renderPlot()}}{
When \code{bindCache()} is used with \code{renderPlot()}, the \code{height} and \code{width}
passed to the original \code{renderPlot()} are ignored. They are superseded by
\code{sizePolicy} argument passed to `bindCache. The default is:\preformatted{sizePolicy = sizeGrowthRatio(width = 400, height = 400, growthRate = 1.2)
}
\code{sizePolicy} must be a function that takes a two-element numeric vector as
input, representing the width and height of the \verb{<img>} element in the
browser window, and it must return a two-element numeric vector, representing
the pixel dimensions of the plot to generate. The purpose is to round the
actual pixel dimensions from the browser to some other dimensions, so that
this will not generate and cache images of every possible pixel dimension.
See \code{\link[=sizeGrowthRatio]{sizeGrowthRatio()}} for more information on the default sizing policy.
}
\examples{
\dontrun{
rc <- bindCache(
x = reactive({
Sys.sleep(2) # Pretend this is expensive
input$x * 100
}),
input$x
)
# Can make it prettier with the \%>\% operator
library(magrittr)
rc <- reactive({
Sys.sleep(2)
input$x * 100
}) \%>\%
bindCache(input$x)
}
## Only run app examples in interactive R sessions
if (interactive()) {
# Basic example
shinyApp(
ui = fluidPage(
sliderInput("x", "x", 1, 10, 5),
sliderInput("y", "y", 1, 10, 5),
div("x * y: "),
verbatimTextOutput("txt")
),
server = function(input, output) {
r <- reactive({
# The value expression is an _expensive_ computation
message("Doing expensive computation...")
Sys.sleep(2)
input$x * input$y
}) \%>\%
bindCache(input$x, input$y)
output$txt <- renderText(r())
}
)
# Caching renderText
shinyApp(
ui = fluidPage(
sliderInput("x", "x", 1, 10, 5),
sliderInput("y", "y", 1, 10, 5),
div("x * y: "),
verbatimTextOutput("txt")
),
server = function(input, output) {
output$txt <- renderText({
message("Doing expensive computation...")
Sys.sleep(2)
input$x * input$y
}) \%>\%
bindCache(input$x, input$y)
}
)
# Demo of using events and caching with an actionButton
shinyApp(
ui = fluidPage(
sliderInput("x", "x", 1, 10, 5),
sliderInput("y", "y", 1, 10, 5),
actionButton("go", "Go"),
div("x * y: "),
verbatimTextOutput("txt")
),
server = function(input, output) {
r <- reactive({
message("Doing expensive computation...")
Sys.sleep(2)
input$x * input$y
}) \%>\%
bindCache(input$x, input$y) \%>\%
bindEvent(input$go)
# The cached, eventified reactive takes a reactive dependency on
# input$go, but doesn't use it for the cache key. It uses input$x and
# input$y for the cache key, but doesn't take a reactive depdency on
# them, because the reactive dependency is superseded by addEvent().
output$txt <- renderText(r())
}
)
}
}
\seealso{
\code{\link[=bindEvent]{bindEvent()}}, \code{\link[=renderCachedPlot]{renderCachedPlot()}} for caching plots.
}

View File

@@ -1,179 +0,0 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/bind-event.R
\name{bindEvent}
\alias{bindEvent}
\title{Make an object respond only to specified reactive events}
\usage{
bindEvent(
x,
...,
ignoreNULL = TRUE,
ignoreInit = FALSE,
once = FALSE,
label = NULL
)
}
\arguments{
\item{x}{An object to wrap so that is triggered only when a the specified
event occurs.}
\item{...}{One or more expressions that represents the event; this can be a
simple reactive value like \code{input$click}, a call to a reactive expression
like \code{dataset()}, or even a complex expression inside curly braces. If
there are multiple expressions in the \code{...}, then it will take a dependency
on all of them.}
\item{ignoreNULL}{Whether the action should be triggered (or value
calculated) when the input is \code{NULL}. See Details.}
\item{ignoreInit}{If \code{TRUE}, then, when the eventified object is first
created/initialized, don't trigger the action or (compute the value). The
default is \code{FALSE}. See Details.}
\item{once}{Used only for observers. Whether this \code{observer} should be
immediately destroyed after the first time that the code in the observer is
run. This pattern is useful when you want to subscribe to a event that
should only happen once.}
\item{label}{A label for the observer or reactive, useful for debugging.}
}
\description{
Modify an object to respond to "event-like" reactive inputs, values, and
expressions. \code{bindEvent()} can be used with reactive expressions, render
functions, and observers. The resulting object takes a reactive dependency on
the \code{...} arguments, and not on the original object's code. This can, for
example, be used to make an observer execute only when a button is pressed.
}
\section{Details}{
Shiny's reactive programming framework is primarily designed for calculated
values (reactive expressions) and side-effect-causing actions (observers)
that respond to \emph{any} of their inputs changing. That's often what is
desired in Shiny apps, but not always: sometimes you want to wait for a
specific action to be taken from the user, like clicking an
\code{\link[=actionButton]{actionButton()}}, before calculating an expression or taking an action. A
reactive value or expression that is used to trigger other calculations in
this way is called an \emph{event}.
These situations demand a more imperative, "event handling" style of
programming that is possible--but not particularly intuitive--using the
reactive programming primitives \code{\link[=observe]{observe()}} and \code{\link[=isolate]{isolate()}}. \code{bindEvent()}
provides a straightforward API for event handling that wraps \code{observe} and
\code{isolate}.
The \code{...} arguments are captured as expressions and combined into an
\strong{event expression}. When this event expression is invalidated (when its
upstream reactive inputs change), that is an \strong{event}, and it will cause
the original object's code to execute.
Use \code{bindEvent()} with \code{observe()} whenever you want to \emph{perform an action}
in response to an event. (Note that "recalculate a value" does not
generally count as performing an action -- use \code{\link[=reactive]{reactive()}} for that.) The
first argument is observer whose code should be executed whenever the event
occurs.
Use \code{bindEvent()} with \code{reactive()} to create a \emph{calculated value} that only
updates in response to an event. This is just like a normal \link[=reactive]{reactive expression} except it ignores all the usual invalidations that
come from its reactive dependencies; it only invalidates in response to the
given event.
\code{bindEvent()} is often used with \code{\link[=bindCache]{bindCache()}}.
}
\section{ignoreNULL and ignoreInit}{
\code{bindEvent()} takes an \code{ignoreNULL} parameter that affects behavior when
the event expression evaluates to \code{NULL} (or in the special case of an
\code{\link[=actionButton]{actionButton()}}, \code{0}). In these cases, if \code{ignoreNULL} is \code{TRUE}, then it
will raise a silent \link[=validate]{validation} error. This is useful behavior
if you don't want to do the action or calculation when your app first
starts, but wait for the user to initiate the action first (like a "Submit"
button); whereas \code{ignoreNULL=FALSE} is desirable if you want to initially
perform the action/calculation and just let the user re-initiate it (like a
"Recalculate" button).
\code{bindEvent()} also takes an \code{ignoreInit} argument. By default, reactive
expressions and observers will run on the first reactive flush after they
are created (except if, at that moment, the event expression evaluates to
\code{NULL} and \code{ignoreNULL} is \code{TRUE}). But when responding to a click of an
action button, it may often be useful to set \code{ignoreInit} to \code{TRUE}. For
example, if you're setting up an observer to respond to a dynamically
created button, then \code{ignoreInit = TRUE} will guarantee that the action
will only be triggered when the button is actually clicked, instead of also
being triggered when it is created/initialized. Similarly, if you're
setting up a reactive that responds to a dynamically created button used to
refresh some data (which is then returned by that \code{reactive}), then you
should use \code{reactive(...) \%>\% bindEvent(..., ignoreInit = TRUE)} if you
want to let the user decide if/when they want to refresh the data (since,
depending on the app, this may be a computationally expensive operation).
Even though \code{ignoreNULL} and \code{ignoreInit} can be used for similar purposes
they are independent from one another. Here's the result of combining
these:
\describe{
\item{\code{ignoreNULL = TRUE} and \code{ignoreInit = FALSE}}{
This is the default. This combination means that reactive/observer code
will run every time that event expression is not
\code{NULL}. If, at the time of creation, the event expression happens
to \emph{not} be \code{NULL}, then the code runs.
}
\item{\code{ignoreNULL = FALSE} and \code{ignoreInit = FALSE}}{
This combination means that reactive/observer code will
run every time no matter what.
}
\item{\code{ignoreNULL = FALSE} and \code{ignoreInit = TRUE}}{
This combination means that reactive/observer code will
\emph{not} run at the time of creation (because \code{ignoreInit = TRUE}),
but it will run every other time.
}
\item{\code{ignoreNULL = TRUE} and \code{ignoreInit = TRUE}}{
This combination means that reactive/observer code will
\emph{not} at the time of creation (because \code{ignoreInit = TRUE}).
After that, the reactive/observer code will run every time that
the event expression is not \code{NULL}.
}
}
}
\section{Types of objects}{
\code{bindEvent()} can be used with reactive expressions, observers, and shiny
render functions.
When \code{bindEvent()} is used with \code{reactive()}, it creates a new reactive
expression object.
When \code{bindEvent()} is used with \code{observe()}, it alters the observer in
place. It can only be used with observers which have not yet executed.
}
\section{Combining events and caching}{
In many cases, it makes sense to use \code{bindEvent()} along with
\code{bindCache()}, because they each can reduce the amount of work done on the
server. For example, you could have \link{sliderInput}s \code{x} and \code{y} and a
\code{reactive()} that performs a time-consuming operation with those values.
Using \code{bindCache()} can speed things up, especially if there are multiple
users. But it might make sense to also not do the computation until the
user sets both \code{x} and \code{y}, and then clicks on an \link{actionButton} named
\code{go}.
To use both caching and events, the object should first be passed to
\code{bindCache()}, then \code{bindEvent()}. For example:\if{html}{\out{<div class="NA">}}\preformatted{r <- reactive(\{
Sys.sleep(2) # Pretend this is an expensive computation
input$x * input$y
\}) \%>\%
bindCache(input$x, input$y) \%>\%
bindEvent(input$go)
}\if{html}{\out{</div>}}
Anything that consumes \code{r()} will take a reactive dependency on the event
expression given to \code{bindEvent()}, and not the cache key expression given to
\code{bindCache()}. In this case, it is just \code{input$go}.
}

View File

@@ -10,7 +10,7 @@ bootstrapLib(theme = NULL)
\item{theme}{One of the following:
\itemize{
\item \code{NULL} (the default), which implies a "stock" build of Bootstrap 3.
\item A \code{\link[bslib:bs_theme]{bslib::bs_theme()}} object. This can be used to replace a stock
\item A \code{\link[bootstraplib:bs_theme]{bootstraplib::bs_theme()}} object. This can be used to replace a stock
build of Bootstrap 3 with a customized version of Bootstrap 3 or higher.
\item A character string pointing to an alternative Bootstrap stylesheet
(normally a css file within the www directory, e.g. \code{www/bootstrap.css}).

View File

@@ -20,7 +20,7 @@ Bootstrap 3.}
\item{theme}{One of the following:
\itemize{
\item \code{NULL} (the default), which implies a "stock" build of Bootstrap 3.
\item A \code{\link[bslib:bs_theme]{bslib::bs_theme()}} object. This can be used to replace a stock
\item A \code{\link[bootstraplib:bs_theme]{bootstraplib::bs_theme()}} object. This can be used to replace a stock
build of Bootstrap 3 with a customized version of Bootstrap 3 or higher.
\item A character string pointing to an alternative Bootstrap stylesheet
(normally a css file within the www directory, e.g. \code{www/bootstrap.css}).

View File

@@ -8,10 +8,7 @@ createRenderFunction(
func,
transform = function(value, session, name, ...) value,
outputFunc = NULL,
outputArgs = NULL,
cacheHint = "auto",
cacheWriteHook = NULL,
cacheReadHook = NULL
outputArgs = NULL
)
}
\arguments{
@@ -29,65 +26,17 @@ JSON-encoded and sent to the browser.}
this render function. This can be used in R Markdown documents to create
complete output widgets out of just the render function.}
\item{outputArgs}{A list of arguments to pass to the \code{uiFunc}. Render
functions should include \code{outputArgs = list()} in their own parameter list,
and pass through the value to \code{markRenderFunction}, to allow app authors to
customize outputs. (Currently, this is only supported for dynamically
generated UIs, such as those created by Shiny code snippets embedded in R
Markdown documents).}
\item{cacheHint}{One of \code{"auto"}, \code{FALSE}, or some other information to
identify this instance for caching using \code{\link[=bindCache]{bindCache()}}. If \code{"auto"}, it
will try to automatically infer caching information. If \code{FALSE}, do not
allow caching for the object. Some render functions (such as \link{renderPlot})
contain internal state that makes them unsuitable for caching.}
\item{cacheWriteHook}{Used if the render function is passed to \code{bindCache()}.
This is an optional callback function to invoke before saving the value
from the render function to the cache. This function must accept one
argument, the value returned from \code{renderFunc}, and should return the value
to store in the cache.}
\item{cacheReadHook}{Used if the render function is passed to \code{bindCache()}.
This is an optional callback function to invoke after reading a value from
the cache (if there is a cache hit). The function will be passed one
argument, the value retrieved from the cache. This can be useful when some
side effect needs to occur for a render function to behave correctly. For
example, some render functions call \code{\link[=createWebDependency]{createWebDependency()}} so that Shiny
is able to serve JS and CSS resources.}
\item{outputArgs}{A list of arguments to pass to the \code{outputFunc}.
Render functions should include \code{outputArgs = list()} in their own
parameter list, and pass through the value as this argument, to allow app
authors to customize outputs. (Currently, this is only supported for
dynamically generated UIs, such as those created by Shiny code snippets
embedded in R Markdown documents).}
}
\value{
An annotated render function, ready to be assigned to an
\code{output} slot.
}
\description{
This function is a wrapper for \code{\link[=markRenderFunction]{markRenderFunction()}} which provides support
for async computation via promises.
}
\examples{
# A very simple render function
renderTriple <- function(x) {
x <- substitute(x)
if (!rlang::is_quosure(x)) {
x <- rlang::new_quosure(x, env = parent.frame())
}
func <- quoToFunction(x, "renderTriple")
createRenderFunction(
func,
transform = function(value, session, name, ...) {
paste(rep(value, 3), collapse=", ")
},
outputFunc = textOutput
)
}
# Test render function from the console
a <- 1
r <- renderTriple({ a + 1 })
a <- 2
r()
}
\seealso{
\code{\link[=quoToFunction]{quoToFunction()}}, \code{\link[=markRenderFunction]{markRenderFunction()}}.
Implement render functions
}

View File

@@ -1,12 +1,12 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/deprecated.R
% Please edit documentation in R/cache-disk.R
\name{diskCache}
\alias{diskCache}
\title{Create disk cache (deprecated)}
\title{Create a disk cache object}
\usage{
diskCache(
dir = NULL,
max_size = 500 * 1024^2,
max_size = 10 * 1024^2,
max_age = Inf,
max_n = Inf,
evict = c("lru", "fifo"),
@@ -17,12 +17,12 @@ diskCache(
)
}
\arguments{
\item{dir}{Directory to store files for the cache. If \code{NULL} (the default) it
will create and use a temporary directory.}
\item{dir}{Directory to store files for the cache. If \code{NULL} (the
default) it will create and use a temporary directory.}
\item{max_size}{Maximum size of the cache, in bytes. If the cache exceeds
this size, cached objects will be removed according to the value of the
\code{evict}. Use \code{Inf} for no size limit. The default is 1 gigabyte.}
\code{evict}. Use \code{Inf} for no size limit.}
\item{max_age}{Maximum age of files in cache before they are evicted, in
seconds. Use \code{Inf} for no age limit.}
@@ -32,25 +32,215 @@ exceeds this value, then cached objects will be removed according to the
value of \code{evict}. Use \code{Inf} for no limit of number of items.}
\item{evict}{The eviction policy to use to decide which objects are removed
when a cache pruning occurs. Currently, \code{"lru"} and \code{"fifo"} are supported.}
when a cache pruning occurs. Currently, \code{"lru"} and \code{"fifo"} are
supported.}
\item{destroy_on_finalize}{If \code{TRUE}, then when the cache_disk object is
\item{destroy_on_finalize}{If \code{TRUE}, then when the DiskCache object is
garbage collected, the cache directory and all objects inside of it will be
deleted from disk. If \code{FALSE} (the default), it will do nothing when
finalized.}
\item{missing}{A value to return when \code{get(key)} is called but the key is not
present in the cache. The default is a \code{\link[cachem:reexports]{key_missing()}} object. It is
actually an expression that is evaluated each time there is a cache miss.
See section Missing keys for more information.}
\item{missing}{A value to return or a function to execute when
\code{get(key)} is called but the key is not present in the cache. The
default is a \code{\link[=key_missing]{key_missing()}} object. If it is a function to
execute, the function must take one argument (the key), and you must also
use \code{exec_missing = TRUE}. If it is a function, it is useful in most
cases for it to throw an error, although another option is to return a
value. If a value is returned, that value will in turn be returned by
\code{get()}. See section Missing keys for more information.}
\item{exec_missing}{Deprecated.}
\item{exec_missing}{If \code{FALSE} (the default), then treat \code{missing}
as a value to return when \code{get()} results in a cache miss. If
\code{TRUE}, treat \code{missing} as a function to execute when
\code{get()} results in a cache miss.}
\item{logfile}{An optional filename or connection object to where logging
information will be written. To log to the console, use \code{stderr()} or
\code{stdout()}.}
information will be written. To log to the console, use \code{stdout()}.}
}
\description{
Create disk cache (deprecated)
A disk cache object is a key-value store that saves the values as files in a
directory on disk. Objects can be stored and retrieved using the \code{get()}
and \code{set()} methods. Objects are automatically pruned from the cache
according to the parameters \code{max_size}, \code{max_age}, \code{max_n},
and \code{evict}.
}
\keyword{internal}
\section{Missing Keys}{
The \code{missing} and \code{exec_missing} parameters controls what happens
when \code{get()} is called with a key that is not in the cache (a cache
miss). The default behavior is to return a \code{\link[=key_missing]{key_missing()}}
object. This is a \emph{sentinel value} that indicates that the key was not
present in the cache. You can test if the returned value represents a
missing key by using the \code{\link[=is.key_missing]{is.key_missing()}} function. You can
also have \code{get()} return a different sentinel value, like \code{NULL}.
If you want to throw an error on a cache miss, you can do so by providing a
function for \code{missing} that takes one argument, the key, and also use
\code{exec_missing=TRUE}.
When the cache is created, you can supply a value for \code{missing}, which
sets the default value to be returned for missing values. It can also be
overridden when \code{get()} is called, by supplying a \code{missing}
argument. For example, if you use \code{cache$get("mykey", missing = NULL)}, it will return \code{NULL} if the key is not in the cache.
If your cache is configured so that \code{get()} returns a sentinel value
to represent a cache miss, then \code{set} will also not allow you to store
the sentinel value in the cache. It will throw an error if you attempt to
do so.
Instead of returning the same sentinel value each time there is cache miss,
the cache can execute a function each time \code{get()} encounters missing
key. If the function returns a value, then \code{get()} will in turn return
that value. However, a more common use is for the function to throw an
error. If an error is thrown, then \code{get()} will not return a value.
To do this, pass a one-argument function to \code{missing}, and use
\code{exec_missing=TRUE}. For example, if you want to throw an error that
prints the missing key, you could do this:
\preformatted{
diskCache(
missing = function(key) {
stop("Attempted to get missing key: ", key)
},
exec_missing = TRUE
)
}
If you use this, the code that calls \code{get()} should be wrapped with
\code{\link[=tryCatch]{tryCatch()}} to gracefully handle missing keys.
}
\section{Cache pruning}{
Cache pruning occurs when \code{set()} is called, or it can be invoked
manually by calling \code{prune()}.
The disk cache will throttle the pruning so that it does not happen on
every call to \code{set()}, because the filesystem operations for checking
the status of files can be slow. Instead, it will prune once in every 20
calls to \code{set()}, or if at least 5 seconds have elapsed since the last
prune occurred, whichever is first. These parameters are currently not
customizable, but may be in the future.
When a pruning occurs, if there are any objects that are older than
\code{max_age}, they will be removed.
The \code{max_size} and \code{max_n} parameters are applied to the cache as
a whole, in contrast to \code{max_age}, which is applied to each object
individually.
If the number of objects in the cache exceeds \code{max_n}, then objects
will be removed from the cache according to the eviction policy, which is
set with the \code{evict} parameter. Objects will be removed so that the
number of items is \code{max_n}.
If the size of the objects in the cache exceeds \code{max_size}, then
objects will be removed from the cache. Objects will be removed from the
cache so that the total size remains under \code{max_size}. Note that the
size is calculated using the size of the files, not the size of disk space
used by the files --- these two values can differ because of files are
stored in blocks on disk. For example, if the block size is 4096 bytes,
then a file that is one byte in size will take 4096 bytes on disk.
Another time that objects can be removed from the cache is when
\code{get()} is called. If the target object is older than \code{max_age},
it will be removed and the cache will report it as a missing value.
}
\section{Eviction policies}{
If \code{max_n} or \code{max_size} are used, then objects will be removed
from the cache according to an eviction policy. The available eviction
policies are:
\describe{
\item{\code{"lru"}}{
Least Recently Used. The least recently used objects will be removed.
This uses the filesystem's mtime property. When "lru" is used, each
\code{get()} is called, it will update the file's mtime.
}
\item{\code{"fifo"}}{
First-in-first-out. The oldest objects will be removed.
}
}
Both of these policies use files' mtime. Note that some filesystems (notably
FAT) have poor mtime resolution. (atime is not used because support for
atime is worse than mtime.)
}
\section{Sharing among multiple processes}{
The directory for a DiskCache can be shared among multiple R processes. To
do this, each R process should have a DiskCache object that uses the same
directory. Each DiskCache will do pruning independently of the others, so if
they have different pruning parameters, then one DiskCache may remove cached
objects before another DiskCache would do so.
Even though it is possible for multiple processes to share a DiskCache
directory, this should not be done on networked file systems, because of
slow performance of networked file systems can cause problems. If you need
a high-performance shared cache, you can use one built on a database like
Redis, SQLite, mySQL, or similar.
When multiple processes share a cache directory, there are some potential
race conditions. For example, if your code calls \code{exists(key)} to check
if an object is in the cache, and then call \code{get(key)}, the object may
be removed from the cache in between those two calls, and \code{get(key)}
will throw an error. Instead of calling the two functions, it is better to
simply call \code{get(key)}, and check that the returned object is not a
\code{key_missing()} object, using \code{is.key_missing()}. This effectively tests for
existence and gets the object in one operation.
It is also possible for one processes to prune objects at the same time that
another processes is trying to prune objects. If this happens, you may see
a warning from \code{file.remove()} failing to remove a file that has
already been deleted.
}
\section{Methods}{
A disk cache object has the following methods:
\describe{
\item{\code{get(key, missing, exec_missing)}}{
Returns the value associated with \code{key}. If the key is not in the
cache, then it returns the value specified by \code{missing} or,
\code{missing} is a function and \code{exec_missing=TRUE}, then
executes \code{missing}. The function can throw an error or return the
value. If either of these parameters are specified here, then they
will override the defaults that were set when the DiskCache object was
created. See section Missing Keys for more information.
}
\item{\code{set(key, value)}}{
Stores the \code{key}-\code{value} pair in the cache.
}
\item{\code{exists(key)}}{
Returns \code{TRUE} if the cache contains the key, otherwise
\code{FALSE}.
}
\item{\code{size()}}{
Returns the number of items currently in the cache.
}
\item{\code{keys()}}{
Returns a character vector of all keys currently in the cache.
}
\item{\code{reset()}}{
Clears all objects from the cache.
}
\item{\code{destroy()}}{
Clears all objects in the cache, and removes the cache directory from
disk.
}
\item{\code{prune()}}{
Prunes the cache, using the parameters specified by \code{max_size},
\code{max_age}, \code{max_n}, and \code{evict}.
}
}
}

View File

@@ -16,8 +16,7 @@ calling environment two steps back.}
}
\description{
This is to be called from another function, because it will attempt to get
an unquoted expression from two calls back. Note: as of Shiny 1.6.0, it is
recommended to use \code{\link[=quoToFunction]{quoToFunction()}} instead.
an unquoted expression from two calls back.
}
\details{
If expr is a quoted expression, then this just converts it to a function.

View File

@@ -8,7 +8,7 @@ getCurrentTheme()
}
\value{
If called at render-time (i.e., inside a \code{\link[htmltools:tagFunction]{htmltools::tagFunction()}}),
and \code{\link[=bootstrapLib]{bootstrapLib()}}'s \code{theme} has been set to a \code{\link[bslib:bs_theme]{bslib::bs_theme()}}
and \code{\link[=bootstrapLib]{bootstrapLib()}}'s \code{theme} has been set to a \code{\link[bootstraplib:bs_theme]{bootstraplib::bs_theme()}}
object, then this returns the \code{theme}. Otherwise, this returns \code{NULL}.
}
\description{

View File

@@ -4,7 +4,7 @@
\alias{icon}
\title{Create an icon}
\usage{
icon(name, class = NULL, lib = "font-awesome", ...)
icon(name, class = NULL, lib = "font-awesome")
}
\arguments{
\item{name}{Name of icon. Icons are drawn from the
@@ -20,8 +20,6 @@ in icon names (i.e. the "fa-calendar" icon should be referred to as
supported styles).}
\item{lib}{Icon library to use ("font-awesome" or "glyphicon")}
\item{...}{Arguments passed to the \verb{<i>} tag of \link[htmltools:builder]{htmltools::tags}}
}
\value{
An icon element

View File

@@ -35,8 +35,7 @@ the name of the calling function.}
}
\description{
Installs an expression in the given environment as a function, and registers
debug hooks so that breakpoints may be set in the function. Note: as of
Shiny 1.6.0, it is recommended to use \code{\link[=quoToFunction]{quoToFunction()}} instead.
debug hooks so that breakpoints may be set in the function.
}
\details{
This function can replace \code{exprToFunction} as follows: we may use

View File

@@ -4,14 +4,7 @@
\alias{markRenderFunction}
\title{Mark a function as a render function}
\usage{
markRenderFunction(
uiFunc,
renderFunc,
outputArgs = list(),
cacheHint = "auto",
cacheWriteHook = NULL,
cacheReadHook = NULL
)
markRenderFunction(uiFunc, renderFunc, outputArgs = list())
}
\arguments{
\item{uiFunc}{A function that renders Shiny UI. Must take a single argument:
@@ -21,42 +14,19 @@ an output ID.}
slot.}
\item{outputArgs}{A list of arguments to pass to the \code{uiFunc}. Render
functions should include \code{outputArgs = list()} in their own parameter list,
and pass through the value to \code{markRenderFunction}, to allow app authors to
customize outputs. (Currently, this is only supported for dynamically
generated UIs, such as those created by Shiny code snippets embedded in R
Markdown documents).}
\item{cacheHint}{One of \code{"auto"}, \code{FALSE}, or some other information to
identify this instance for caching using \code{\link[=bindCache]{bindCache()}}. If \code{"auto"}, it
will try to automatically infer caching information. If \code{FALSE}, do not
allow caching for the object. Some render functions (such as \link{renderPlot})
contain internal state that makes them unsuitable for caching.}
\item{cacheWriteHook}{Used if the render function is passed to \code{bindCache()}.
This is an optional callback function to invoke before saving the value
from the render function to the cache. This function must accept one
argument, the value returned from \code{renderFunc}, and should return the value
to store in the cache.}
\item{cacheReadHook}{Used if the render function is passed to \code{bindCache()}.
This is an optional callback function to invoke after reading a value from
the cache (if there is a cache hit). The function will be passed one
argument, the value retrieved from the cache. This can be useful when some
side effect needs to occur for a render function to behave correctly. For
example, some render functions call \code{\link[=createWebDependency]{createWebDependency()}} so that Shiny
is able to serve JS and CSS resources.}
functions should include \code{outputArgs = list()} in their own parameter
list, and pass through the value to \code{markRenderFunction}, to allow
app authors to customize outputs. (Currently, this is only supported for
dynamically generated UIs, such as those created by Shiny code snippets
embedded in R Markdown documents).}
}
\value{
The \code{renderFunc} function, with annotations.
}
\description{
Should be called by implementers of \code{renderXXX} functions in order to mark
their return values as Shiny render functions, and to provide a hint to Shiny
regarding what UI function is most commonly used with this type of render
function. This can be used in R Markdown documents to create complete output
widgets out of just the render function.
}
\seealso{
\code{\link[=createRenderFunction]{createRenderFunction()}}, \code{\link[=quoToFunction]{quoToFunction()}}
Should be called by implementers of \code{renderXXX} functions in order to
mark their return values as Shiny render functions, and to provide a hint to
Shiny regarding what UI function is most commonly used with this type of
render function. This can be used in R Markdown documents to create complete
output widgets out of just the render function.
}

View File

@@ -1,11 +1,11 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/deprecated.R
% Please edit documentation in R/cache-memory.R
\name{memoryCache}
\alias{memoryCache}
\title{Create memory cache (deprecated)}
\title{Create a memory cache object}
\usage{
memoryCache(
max_size = 200 * 1024^2,
max_size = 10 * 1024^2,
max_age = Inf,
max_n = Inf,
evict = c("lru", "fifo"),
@@ -17,7 +17,7 @@ memoryCache(
\arguments{
\item{max_size}{Maximum size of the cache, in bytes. If the cache exceeds
this size, cached objects will be removed according to the value of the
\code{evict}. Use \code{Inf} for no size limit. The default is 1 gigabyte.}
\code{evict}. Use \code{Inf} for no size limit.}
\item{max_age}{Maximum age of files in cache before they are evicted, in
seconds. Use \code{Inf} for no age limit.}
@@ -27,20 +27,178 @@ exceeds this value, then cached objects will be removed according to the
value of \code{evict}. Use \code{Inf} for no limit of number of items.}
\item{evict}{The eviction policy to use to decide which objects are removed
when a cache pruning occurs. Currently, \code{"lru"} and \code{"fifo"} are supported.}
when a cache pruning occurs. Currently, \code{"lru"} and \code{"fifo"} are
supported.}
\item{missing}{A value to return when \code{get(key)} is called but the key is not
present in the cache. The default is a \code{\link[cachem:reexports]{key_missing()}} object. It is
actually an expression that is evaluated each time there is a cache miss.
See section Missing keys for more information.}
\item{missing}{A value to return or a function to execute when
\code{get(key)} is called but the key is not present in the cache. The
default is a \code{\link[=key_missing]{key_missing()}} object. If it is a function to
execute, the function must take one argument (the key), and you must also
use \code{exec_missing = TRUE}. If it is a function, it is useful in most
cases for it to throw an error, although another option is to return a
value. If a value is returned, that value will in turn be returned by
\code{get()}. See section Missing keys for more information.}
\item{exec_missing}{Deprecated.}
\item{exec_missing}{If \code{FALSE} (the default), then treat \code{missing}
as a value to return when \code{get()} results in a cache miss. If
\code{TRUE}, treat \code{missing} as a function to execute when
\code{get()} results in a cache miss.}
\item{logfile}{An optional filename or connection object to where logging
information will be written. To log to the console, use \code{stderr()} or
\code{stdout()}.}
information will be written. To log to the console, use \code{stdout()}.}
}
\description{
Create memory cache (deprecated)
A memory cache object is a key-value store that saves the values in an
environment. Objects can be stored and retrieved using the \code{get()} and
\code{set()} methods. Objects are automatically pruned from the cache
according to the parameters \code{max_size}, \code{max_age}, \code{max_n},
and \code{evict}.
}
\keyword{internal}
\details{
In a \code{MemoryCache}, R objects are stored directly in the cache; they are
not \emph{not} serialized before being stored in the cache. This contrasts
with other cache types, like \code{\link[=diskCache]{diskCache()}}, where objects are
serialized, and the serialized object is cached. This can result in some
differences of behavior. For example, as long as an object is stored in a
MemoryCache, it will not be garbage collected.
}
\section{Missing keys}{
The \code{missing} and \code{exec_missing} parameters controls what happens
when \code{get()} is called with a key that is not in the cache (a cache
miss). The default behavior is to return a \code{\link[=key_missing]{key_missing()}}
object. This is a \emph{sentinel value} that indicates that the key was not
present in the cache. You can test if the returned value represents a
missing key by using the \code{\link[=is.key_missing]{is.key_missing()}} function. You can
also have \code{get()} return a different sentinel value, like \code{NULL}.
If you want to throw an error on a cache miss, you can do so by providing a
function for \code{missing} that takes one argument, the key, and also use
\code{exec_missing=TRUE}.
When the cache is created, you can supply a value for \code{missing}, which
sets the default value to be returned for missing values. It can also be
overridden when \code{get()} is called, by supplying a \code{missing}
argument. For example, if you use \code{cache$get("mykey", missing = NULL)}, it will return \code{NULL} if the key is not in the cache.
If your cache is configured so that \code{get()} returns a sentinel value
to represent a cache miss, then \code{set} will also not allow you to store
the sentinel value in the cache. It will throw an error if you attempt to
do so.
Instead of returning the same sentinel value each time there is cache miss,
the cache can execute a function each time \code{get()} encounters missing
key. If the function returns a value, then \code{get()} will in turn return
that value. However, a more common use is for the function to throw an
error. If an error is thrown, then \code{get()} will not return a value.
To do this, pass a one-argument function to \code{missing}, and use
\code{exec_missing=TRUE}. For example, if you want to throw an error that
prints the missing key, you could do this:
\preformatted{
diskCache(
missing = function(key) {
stop("Attempted to get missing key: ", key)
},
exec_missing = TRUE
)
}
If you use this, the code that calls \code{get()} should be wrapped with
\code{\link[=tryCatch]{tryCatch()}} to gracefully handle missing keys.
}
\section{Cache pruning}{
Cache pruning occurs when \code{set()} is called, or it can be invoked
manually by calling \code{prune()}.
When a pruning occurs, if there are any objects that are older than
\code{max_age}, they will be removed.
The \code{max_size} and \code{max_n} parameters are applied to the cache as
a whole, in contrast to \code{max_age}, which is applied to each object
individually.
If the number of objects in the cache exceeds \code{max_n}, then objects
will be removed from the cache according to the eviction policy, which is
set with the \code{evict} parameter. Objects will be removed so that the
number of items is \code{max_n}.
If the size of the objects in the cache exceeds \code{max_size}, then
objects will be removed from the cache. Objects will be removed from the
cache so that the total size remains under \code{max_size}. Note that the
size is calculated using the size of the files, not the size of disk space
used by the files --- these two values can differ because of files are
stored in blocks on disk. For example, if the block size is 4096 bytes,
then a file that is one byte in size will take 4096 bytes on disk.
Another time that objects can be removed from the cache is when
\code{get()} is called. If the target object is older than \code{max_age},
it will be removed and the cache will report it as a missing value.
}
\section{Eviction policies}{
If \code{max_n} or \code{max_size} are used, then objects will be removed
from the cache according to an eviction policy. The available eviction
policies are:
\describe{
\item{\code{"lru"}}{
Least Recently Used. The least recently used objects will be removed.
This uses the filesystem's atime property. Some filesystems do not
support atime, or have a very low atime resolution. The DiskCache will
check for atime support, and if the filesystem does not support atime,
a warning will be issued and the "fifo" policy will be used instead.
}
\item{\code{"fifo"}}{
First-in-first-out. The oldest objects will be removed.
}
}
}
\section{Methods}{
A disk cache object has the following methods:
\describe{
\item{\code{get(key, missing, exec_missing)}}{
Returns the value associated with \code{key}. If the key is not in the
cache, then it returns the value specified by \code{missing} or,
\code{missing} is a function and \code{exec_missing=TRUE}, then
executes \code{missing}. The function can throw an error or return the
value. If either of these parameters are specified here, then they
will override the defaults that were set when the DiskCache object was
created. See section Missing Keys for more information.
}
\item{\code{set(key, value)}}{
Stores the \code{key}-\code{value} pair in the cache.
}
\item{\code{exists(key)}}{
Returns \code{TRUE} if the cache contains the key, otherwise
\code{FALSE}.
}
\item{\code{size()}}{
Returns the number of items currently in the cache.
}
\item{\code{keys()}}{
Returns a character vector of all keys currently in the cache.
}
\item{\code{reset()}}{
Clears all objects from the cache.
}
\item{\code{destroy()}}{
Clears all objects in the cache, and removes the cache directory from
disk.
}
\item{\code{prune()}}{
Prunes the cache, using the parameters specified by \code{max_size},
\code{max_age}, \code{max_n}, and \code{evict}.
}
}
}

View File

@@ -8,7 +8,6 @@ observe(
x,
env = parent.frame(),
quoted = FALSE,
...,
label = NULL,
suspended = FALSE,
priority = 0,
@@ -29,8 +28,6 @@ non-reactive expression.}
This is useful when you want to use an expression that is stored in a
variable; to do so, it must be quoted with \code{quote()}.}
\item{...}{Not used.}
\item{label}{A label for the observer, useful for debugging.}
\item{suspended}{If \code{TRUE}, start the observer in a suspended state. If

View File

@@ -12,7 +12,6 @@ observeEvent(
event.quoted = FALSE,
handler.env = parent.frame(),
handler.quoted = FALSE,
...,
label = NULL,
suspended = FALSE,
priority = 0,
@@ -30,7 +29,6 @@ eventReactive(
event.quoted = FALSE,
value.env = parent.frame(),
value.quoted = FALSE,
...,
label = NULL,
domain = getDefaultReactiveDomain(),
ignoreNULL = TRUE,
@@ -64,8 +62,6 @@ default, this is \code{FALSE}. This is useful when you want to use an
expression that is stored in a variable; to do so, it must be quoted with
\code{quote()}.}
\item{...}{Currently not used.}
\item{label}{A label for the observer or reactive, useful for debugging.}
\item{suspended}{If \code{TRUE}, start the observer in a suspended state. If

View File

@@ -1,31 +0,0 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/utils.R
\name{quoToFunction}
\alias{quoToFunction}
\title{Convert a quosure to a function for a Shiny render function}
\usage{
quoToFunction(q, label, ..stacktraceon = FALSE)
}
\arguments{
\item{q}{A quosure.}
\item{label}{A label for the object to be shown in the debugger. Defaults to
the name of the calling function.}
\item{..stacktraceon}{Advanced use only. For stack manipulation purposes; see
\code{\link[=stacktrace]{stacktrace()}}.}
}
\description{
This takes a quosure and label, and wraps them into a function that should be
passed to \code{\link[=createRenderFunction]{createRenderFunction()}} or \code{\link[=markRenderFunction]{markRenderFunction()}}.
}
\details{
This function was added in Shiny 1.6.0. Previously, it was recommended to use
\code{\link[=installExprFunction]{installExprFunction()}} or \code{\link[=exprToFunction]{exprToFunction()}} in render functions, but now we
recommend using \code{\link[=quoToFunction]{quoToFunction()}}, because it does not require \code{env} and
\code{quoted} arguments -- that information is captured by quosures provided by
\pkg{rlang}.
}
\seealso{
\code{\link[=createRenderFunction]{createRenderFunction()}} for example usage.
}

View File

@@ -9,7 +9,6 @@ reactive(
x,
env = parent.frame(),
quoted = FALSE,
...,
label = NULL,
domain = getDefaultReactiveDomain(),
..stacktraceon = TRUE
@@ -29,8 +28,6 @@ non-reactive expression.}
This is useful when you want to use an expression that is stored in a
variable; to do so, it must be quoted with \code{quote()}.}
\item{...}{Not used.}
\item{label}{A label for the reactive expression, useful for debugging.}
\item{domain}{See \link{domains}.}

View File

@@ -33,9 +33,10 @@ information on the default sizing policy.}
\item{res}{The resolution of the PNG, in pixels per inch.}
\item{cache}{The scope of the cache, or a cache object. This can be \code{"app"}
(the default), \code{"session"}, or a cache object like a
\code{\link[cachem:cache_disk]{cachem::cache_disk()}}. See the Cache Scoping section for more information.}
\item{cache}{The scope of the cache, or a cache object. This can be
\code{"app"} (the default), \code{"session"}, or a cache object like
a \code{\link[=diskCache]{diskCache()}}. See the Cache Scoping section for more
information.}
\item{...}{Arguments to be passed through to \code{\link[grDevices:png]{grDevices::png()}}.
These can be used to set the width, height, background color, etc.}
@@ -55,8 +56,7 @@ interactive R Markdown document.}
\code{sizePolicy}.}
}
\description{
Renders a reactive plot, with plot images cached to disk. As of Shiny 1.6.0,
this is a shortcut for using \code{\link[=bindCache]{bindCache()}} with \code{\link[=renderPlot]{renderPlot()}}.
Renders a reactive plot, with plot images cached to disk.
}
\details{
\code{expr} is an expression that generates a plot, similar to that in
@@ -97,6 +97,97 @@ with \code{"plot1"}) to form the actual key that is used. As a result, even
if there are multiple plots that have the same \code{cacheKeyExpr}, they
will not have cache key collisions.
}
\section{Cache scoping}{
There are a number of different ways you may want to scope the cache. For
example, you may want each user session to have their own plot cache, or
you may want each run of the application to have a cache (shared among
possibly multiple simultaneous user sessions), or you may want to have a
cache that persists even after the application is shut down and started
again.
To control the scope of the cache, use the \code{cache} parameter. There
are two ways of having Shiny automatically create and clean up the disk
cache.
\describe{
\item{1}{To scope the cache to one run of a Shiny application (shared
among possibly multiple user sessions), use \code{cache="app"}. This
is the default. The cache will be shared across multiple sessions, so
there is potentially a large performance benefit if there are many users
of the application. When the application stops running, the cache will
be deleted. If plots cannot be safely shared across users, this should
not be used.}
\item{2}{To scope the cache to one session, use \code{cache="session"}.
When a new user session starts --- in other words, when a web browser
visits the Shiny application --- a new cache will be created on disk
for that session. When the session ends, the cache will be deleted.
The cache will not be shared across multiple sessions.}
}
If either \code{"app"} or \code{"session"} is used, the cache will be 10 MB
in size, and will be stored stored in memory, using a
\code{\link[=memoryCache]{memoryCache()}} object. Note that the cache space will be shared
among all cached plots within a single application or session.
In some cases, you may want more control over the caching behavior. For
example, you may want to use a larger or smaller cache, share a cache
among multiple R processes, or you may want the cache to persist across
multiple runs of an application, or even across multiple R processes.
To use different settings for an application-scoped cache, you can call
\code{\link[=shinyOptions]{shinyOptions()}} at the top of your app.R, server.R, or
global.R. For example, this will create a cache with 20 MB of space
instead of the default 10 MB:
\preformatted{
shinyOptions(cache = memoryCache(size = 20e6))
}
To use different settings for a session-scoped cache, you can call
\code{\link[=shinyOptions]{shinyOptions()}} at the top of your server function. To use
the session-scoped cache, you must also call \code{renderCachedPlot} with
\code{cache="session"}. This will create a 20 MB cache for the session:
\preformatted{
function(input, output, session) {
shinyOptions(cache = memoryCache(size = 20e6))
output$plot <- renderCachedPlot(
...,
cache = "session"
)
}
}
If you want to create a cache that is shared across multiple concurrent
R processes, you can use a \code{\link[=diskCache]{diskCache()}}. You can create an
application-level shared cache by putting this at the top of your app.R,
server.R, or global.R:
\preformatted{
shinyOptions(cache = diskCache(file.path(dirname(tempdir()), "myapp-cache"))
}
This will create a subdirectory in your system temp directory named
\code{myapp-cache} (replace \code{myapp-cache} with a unique name of
your choosing). On most platforms, this directory will be removed when
your system reboots. This cache will persist across multiple starts and
stops of the R process, as long as you do not reboot.
To have the cache persist even across multiple reboots, you can create the
cache in a location outside of the temp directory. For example, it could
be a subdirectory of the application:
\preformatted{
shinyOptions(cache = diskCache("./myapp-cache"))
}
In this case, resetting the cache will have to be done manually, by deleting
the directory.
You can also scope a cache to just one plot, or selected plots. To do that,
create a \code{\link[=memoryCache]{memoryCache()}} or \code{\link[=diskCache]{diskCache()}}, and pass it
as the \code{cache} argument of \code{renderCachedPlot}.
}
\section{Interactive plots}{
@@ -192,7 +283,7 @@ shinyApp(
xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
},
cacheKeyExpr = { list(input$n) },
cache = cachem::cache_mem()
cache = memoryCache()
)
output$plot2 <- renderCachedPlot({
Sys.sleep(2) # Add an artificial delay
@@ -201,7 +292,7 @@ shinyApp(
xlim = range(mtcars$wt), ylim = range(mtcars$mpg))
},
cacheKeyExpr = { list(input$n) },
cache = cachem::cache_mem()
cache = memoryCache()
)
}
)
@@ -212,22 +303,22 @@ shinyApp(
# At the top of app.R, this set the application-scoped cache to be a memory
# cache that is 20 MB in size, and where cached objects expire after one
# hour.
shinyOptions(cache = cachem::cache_mem(max_size = 20e6, max_age = 3600))
shinyOptions(cache = memoryCache(max_size = 20e6, max_age = 3600))
# At the top of app.R, this set the application-scoped cache to be a disk
# cache that can be shared among multiple concurrent R processes, and is
# deleted when the system reboots.
shinyOptions(cache = cachem::cache_disk(file.path(dirname(tempdir()), "myapp-cache"))
shinyOptions(cache = diskCache(file.path(dirname(tempdir()), "myapp-cache"))
# At the top of app.R, this set the application-scoped cache to be a disk
# cache that can be shared among multiple concurrent R processes, and
# persists on disk across reboots.
shinyOptions(cache = cachem::cache_disk("./myapp-cache"))
shinyOptions(cache = diskCache("./myapp-cache"))
# At the top of the server function, this set the session-scoped cache to be
# a memory cache that is 5 MB in size.
server <- function(input, output, session) {
shinyOptions(cache = cachem::cache_mem(max_size = 5e6))
shinyOptions(cache = memoryCache(max_size = 5e6))
output$plot <- renderCachedPlot(
...,
@@ -238,8 +329,7 @@ server <- function(input, output, session) {
}
}
\seealso{
See \code{\link[=renderPlot]{renderPlot()}} for the regular, non-cached version of this
function. It can be used with \code{\link[=bindCache]{bindCache()}} to get the same effect as
\code{renderCachedPlot()}. For more about configuring caches, see
\code{\link[cachem:cache_mem]{cachem::cache_mem()}} and \code{\link[cachem:cache_disk]{cachem::cache_disk()}}.
See \code{\link[=renderPlot]{renderPlot()}} for the regular, non-cached version of
this function. For more about configuring caches, see
\code{\link[=memoryCache]{memoryCache()}} and \code{\link[=diskCache]{diskCache()}}.
}

View File

@@ -171,18 +171,6 @@ possible to specify by name which values to return by providing a
character vector, as in \code{input=c("x", "y")}. The format can be
"rds" or "json".
}
\item{setCurrentTheme(theme)}{
Sets the current \code{\link[=bootstrapLib]{bootstrapLib()}} theme, which updates the value of
\code{\link[=getCurrentTheme]{getCurrentTheme()}}, invalidates \code{session$getCurrentTheme()}, and calls
function(s) registered with \code{\link[=registerThemeDependency]{registerThemeDependency()}} with provided
\code{theme}. If those function calls return \code{\link[htmltools:htmlDependency]{htmltools::htmlDependency()}}s with
\code{stylesheet}s, then those stylesheets are "refreshed" (i.e., the new
stylesheets are inserted on the page and the old ones are disabled and
removed).
}
\item{getCurrentTheme()}{
A reactive read of the current \code{\link[=bootstrapLib]{bootstrapLib()}} theme.
}
}
\description{
Shiny server functions can optionally include \code{session} as a parameter

View File

@@ -1,5 +1,5 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/deprecated.R
% Please edit documentation in R/utils.R
\name{shinyDeprecated}
\alias{shinyDeprecated}
\title{Print message for deprecated functions in Shiny}

View File

@@ -147,7 +147,6 @@ can be set globally with \code{options()} or locally (for a single app) with
\code{shinyOptions()}.
\describe{ \item{cache}{A caching object that will be used by
\code{\link[=renderCachedPlot]{renderCachedPlot()}}. If not specified, a \code{\link[cachem:cache_mem]{cachem::cache_mem()}} will be
used.} }
\code{\link[=renderCachedPlot]{renderCachedPlot()}}. If not specified, a \code{\link[=memoryCache]{memoryCache()}} will be used.} }
}

View File

@@ -14,22 +14,17 @@ var browser = (function() {
}
// Detect IE information
var ua = window.navigator.userAgent;
var isIE = /MSIE|Trident/.test(ua);
var isIE = (navigator.appName === 'Microsoft Internet Explorer');
function getIEVersion() {
var msie = ua.indexOf('MSIE ');
if (isIE && msie > 0) {
// IE 10 or older => return version number
return parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10);
var rv = -1;
if (isIE) {
var ua = navigator.userAgent;
var re = new RegExp("MSIE ([0-9]{1,}[\\.0-9]{0,})");
if (re.exec(ua) !== null)
rv = parseFloat(RegExp.$1);
}
var trident = ua.indexOf('Trident/');
if (trident > 0) {
// IE 11 => return version number
var rv = ua.indexOf('rv:');
return parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10);
}
return -1;
return rv;
}
return {

View File

@@ -107,76 +107,23 @@ function renderDependency(dep) {
}
if (dep.stylesheet) {
var links = $.map(asArray(dep.stylesheet), function(stylesheet) {
return $("<link rel='stylesheet' type='text/css'>").attr("href", href + "/" + encodeURI(stylesheet));
var stylesheets = $.map(asArray(dep.stylesheet), function(stylesheet) {
var this_href = href + "/" + encodeURI(stylesheet);
// If a styleSheet with this href is already active, then disable it
var re = new RegExp(this_href + "$");
for (var i = 0; i < document.styleSheets.length; i++) {
var h = document.styleSheets[i].href;
if (!h) continue;
if (restyle && h.match(re)) document.styleSheets[i].disabled = true;
}
// Force client to re-request the stylesheet if we've already seen it
// (without this unique param, the request might get cached)
if (restyle) {
this_href = this_href + "?restyle=" + new Date().getTime();
}
return $("<link rel='stylesheet' type='text/css'>").attr("href", this_href);
});
if (!restyle) {
$head.append(links);
} else {
$.map(links, function(link) {
// Find any document.styleSheets that match this link's href
// so we can remove it after bringing in the new stylesheet
var oldSheet = findSheet(link.attr("href"));
// Add a timestamp to the href to prevent caching
var href = link.attr("href") + "?restyle=" + new Date().getTime();
// Use inline <style> approach for IE, otherwise use the more elegant
// <link> -based approach
if (browser.isIE) {
refreshStyle(href, oldSheet);
} else {
link.attr("href", href);
// Once the new <link> is loaded, schedule the old <link> to be removed
// on the next tick which is needed to avoid FOUC
link.attr("onload", () => {
setTimeout(() => removeSheet(oldSheet), 500);
});
$head.append(link);
}
});
// Once the new styles are applied, CSS values that are accessible server-side
// (e.g., getCurrentOutputInfo(), output visibility, etc) may become outdated.
// At the time of writing, that means we need to do sendImageSize() &
// sendOutputHiddenState() again, which can be done by re-binding.
/* global Shiny */
var bindDebouncer = new Debouncer(null, Shiny.bindAll, 100);
setTimeout(() => bindDebouncer.normalCall(), 100);
// This inline <style> based approach works for IE11
function refreshStyle(href, oldSheet) {
var xhr = new XMLHttpRequest();
xhr.open('GET', href);
xhr.onload = function() {
var id = "shiny_restyle_" + href.split("?restyle")[0].replace(/\W/g, '_');
var oldStyle = $head.find("style#" + id);
var newStyle = $("<style>").attr("id", id).html(xhr.responseText);
$head.append(newStyle);
setTimeout(() => oldStyle.remove(), 500);
setTimeout(() => removeSheet(oldSheet), 500);
};
xhr.send();
}
function findSheet(href) {
for (var i = 0; i < document.styleSheets.length; i++) {
var sheet = document.styleSheets[i];
// The sheet's href is a full URL
if (typeof sheet.href === "string" && sheet.href.indexOf(href) > -1) {
return sheet;
}
}
return null;
}
function removeSheet(sheet) {
if (!sheet) return;
sheet.disabled = true;
if (browser.isIE) sheet.cssText = "";
$(sheet.ownerNode).remove();
}
}
$head.append(stylesheets);
}
if (dep.script && !restyle) {

View File

@@ -149,7 +149,7 @@ function makeBlob(parts) {
function pixelRatio() {
if (window.devicePixelRatio) {
return Math.round(window.devicePixelRatio * 100) / 100;
return window.devicePixelRatio;
} else {
return 1;
}

Submodule tests/testthat/apps deleted from c471e6449e

File diff suppressed because it is too large Load Diff

View File

@@ -1,87 +0,0 @@
# Note that there are some tests for bindEvent() and caching in
# test-with-cache.R.
test_that("bindEvent and observers", {
trigger <- reactiveVal(1)
val <- reactiveVal(10)
vals <- numeric()
o <- bindEvent(
trigger(),
x = observe({
vals <<- c(vals, val())
})
)
flushReact()
expect_identical(vals, 10)
# Changing val has no effect
val(20)
flushReact()
expect_identical(vals, 10)
# Changing trigger causes the observer to execute
trigger(2)
flushReact()
expect_identical(vals, c(10, 20))
trigger(3)
flushReact()
expect_identical(vals, c(10, 20, 20))
})
test_that("bindEvent alters observers in place", {
v <- reactiveVal(1)
o <- observe({ v() })
o1 <- bindEvent(o, v())
# o and o1 are the same object
expect_identical(o, o1)
# Can't call bindEvent twice on an observer
expect_error(bindEvent(o, v()))
})
test_that("ignoreNULL works", {
n <- 0
observe({ n <<- n+1 }) %>% bindEvent(NULL, ignoreNULL = FALSE)
flushReact()
expect_identical(n, 1)
n <- 0
observe({ n <<- n+1 }) %>% bindEvent(NULL, ignoreNULL = TRUE)
flushReact()
expect_identical(n, 0)
# Two NULLs in the `...` get aggregated into a list, so the result is not
# NULL.
n <- 0
observe({ n <<- n+1 }) %>% bindEvent(NULL, NULL, ignoreNULL = TRUE)
flushReact()
expect_identical(n, 1)
})
test_that("once=TRUE works", {
n <- 0
v <- reactiveVal(1)
observe({ n <<- n + 1 }) %>% bindEvent(v(), once = FALSE)
flushReact()
expect_identical(n, 1)
v(2)
flushReact()
expect_identical(n, 2)
n <- 0
v <- reactiveVal(1)
observe({ n <<- n + v() }) %>% bindEvent(v(), once = TRUE)
flushReact()
expect_identical(n, 1)
v(2)
flushReact()
expect_identical(n, 1)
})

103
tests/testthat/test-cache.R Normal file
View File

@@ -0,0 +1,103 @@
context("Cache")
test_that("DiskCache: handling missing values", {
d <- diskCache()
expect_true(is.key_missing(d$get("abcd")))
d$set("a", 100)
expect_identical(d$get("a"), 100)
expect_identical(d$get("y", missing = NULL), NULL)
expect_error(
d$get("y", missing = function(key) stop("Missing key: ", key), exec_missing = TRUE),
"^Missing key: y$",
)
d <- diskCache(missing = NULL)
expect_true(is.null(d$get("abcd")))
d$set("a", 100)
expect_identical(d$get("a"), 100)
expect_identical(d$get("y", missing = -1), -1)
expect_error(
d$get("y", missing = function(key) stop("Missing key: ", key), exec_missing = TRUE),
"^Missing key: y$",
)
d <- diskCache(missing = function(key) stop("Missing key: ", key), exec_missing = TRUE)
expect_error(d$get("abcd"), "^Missing key: abcd$")
# When exec_missing=TRUE, should be able to set a value that's identical to
# missing. Need to suppress warnings, because it will warn about reference
# object (the environment captured by the function)
d$set("x", NULL)
suppressWarnings(d$set("x", function(key) stop("Missing key: ", key)))
d$set("a", 100)
expect_identical(d$get("a"), 100)
expect_identical(d$get("y", missing = NULL, exec_missing = FALSE), NULL)
expect_true(is.key_missing(d$get("y", missing = key_missing(), exec_missing = FALSE)))
expect_equal(d$get("y", exec_missing = FALSE), function(key) stop("Missing key: ", key))
expect_error(
d$get("y", missing = function(key) stop("Missing key 2: ", key), exec_missing = TRUE),
"^Missing key 2: y$",
)
# Can't use exec_missing when missing is not a function
expect_error(diskCache(missing = 1, exec_missing = TRUE))
})
# Issue #3033
test_that("DiskCache: pruning respects both max_n and max_size", {
d <- diskCache(max_n = 3, max_size = 200)
# Set some values. Use rnorm so that object size is large; a simple vector
# like 1:100 will be stored very efficiently by R's ALTREP, and won't exceed
# the max_size. We want each of these objects to exceed max_size so that
# they'll be pruned.
d$set("a", rnorm(100))
d$set("b", rnorm(100))
d$set("c", rnorm(100))
d$set("d", rnorm(100))
d$set("e", rnorm(100))
Sys.sleep(0.1) # For systems that have low mtime resolution.
d$set("f", 1) # This object is small and shouldn't be pruned.
d$prune()
expect_identical(d$keys(), "f")
})
test_that("MemoryCache: handling missing values", {
d <- memoryCache()
expect_true(is.key_missing(d$get("abcd")))
d$set("a", 100)
expect_identical(d$get("a"), 100)
expect_identical(d$get("y", missing = NULL), NULL)
expect_error(
d$get("y", missing = function(key) stop("Missing key: ", key), exec_missing = TRUE),
"^Missing key: y$",
)
d <- memoryCache(missing = NULL)
expect_true(is.null(d$get("abcd")))
d$set("a", 100)
expect_identical(d$get("a"), 100)
expect_identical(d$get("y", missing = -1), -1)
expect_error(
d$get("y", missing = function(key) stop("Missing key: ", key), exec_missing = TRUE),
"^Missing key: y$",
)
d <- memoryCache(missing = function(key) stop("Missing key: ", key), exec_missing = TRUE)
expect_error(d$get("abcd"), "^Missing key: abcd$")
# When exec_missing==TRUE, should be able to set a value that's identical to
# missing.
d$set("x", NULL)
d$set("x", function(key) stop("Missing key: ", key))
d$set("a", 100)
expect_identical(d$get("a"), 100)
expect_identical(d$get("y", missing = NULL, exec_missing = FALSE), NULL)
expect_true(is.key_missing(d$get("y", missing = key_missing(), exec_missing = FALSE)))
expect_error(
d$get("y", missing = function(key) stop("Missing key 2: ", key), exec_missing = TRUE),
"^Missing key 2: y$",
)
# Can't create a cache with both missing and missing_f
expect_error(memoryCache(missing = 1, exec_missing = TRUE))
})

View File

@@ -1,74 +0,0 @@
test_that("hybrid_chain preserves visibility", {
expect_identical(
hybrid_chain(1, withVisible),
list(value = 1, visible = TRUE)
)
expect_identical(
hybrid_chain(invisible(1), withVisible),
list(value = 1, visible = FALSE)
)
expect_identical(
hybrid_chain(1, identity, withVisible),
list(value = 1, visible = TRUE)
)
expect_identical(
hybrid_chain(invisible(1), identity, withVisible),
list(value = 1, visible = FALSE)
)
expect_identical(
hybrid_chain(1, function(x) invisible(x), withVisible),
list(value = 1, visible = FALSE)
)
})
test_that("hybrid_chain preserves visibility - async", {
res <- NULL
hybrid_chain(
promise_resolve(1),
function(value) res <<- withVisible(value)
)
later::run_now()
expect_identical(res, list(value = 1, visible = TRUE))
res <- NULL
hybrid_chain(
promise_resolve(invisible(1)),
function(value) res <<- withVisible(value)
)
later::run_now()
expect_identical(res, list(value = 1, visible = FALSE))
res <- NULL
hybrid_chain(
promise_resolve(1),
identity,
function(value) res <<- withVisible(value)
)
for (i in 1:2) later::run_now()
expect_identical(res, list(value = 1, visible = TRUE))
res <- NULL
hybrid_chain(
promise_resolve(invisible(1)),
identity,
function(value) res <<- withVisible(value)
)
for (i in 1:2) later::run_now()
expect_identical(res, list(value = 1, visible = FALSE))
res <- NULL
hybrid_chain(
promise_resolve(1),
function(x) invisible(x),
function(value) res <<- withVisible(value)
)
for (i in 1:2) later::run_now()
expect_identical(res, list(value = 1, visible = FALSE))
})

View File

@@ -56,25 +56,22 @@ test_that("reactivePoll supported", {
expect_equal(i, 2)
})
# `renderCachedPlot()` is now implemented with `bindCache()`, and won't work by
# calling `f(session, "name")`, because the key computation function is not
# called with session and name.
# test_that("renderCachedPlot supported", {
# session <- MockShinySession$new()
# isolate({
# # renderCachedPlot is sensitive to having the cache set for it before entering.
# origCache <- getShinyOption("cache")
# shinyOptions(cache = cachem::cache_mem())
# on.exit(shinyOptions(cache = origCache), add=TRUE)
#
# p <- renderCachedPlot({ plot(1,1) }, { Sys.time() })
# plt <- p(session, "name")
#
# # Should have a size defined
# expect_equal(plt$coordmap$dims$width, 692) #FIXME: why isn't this respecting the clientdata sizes?
# expect_equal(plt$coordmap$dims$height, 400)
# })
# })
test_that("renderCachedPlot supported", {
session <- MockShinySession$new()
isolate({
# renderCachedPlot is sensitive to having the cache set for it before entering.
origCache <- getShinyOption("cache")
shinyOptions(cache = MemoryCache$new())
on.exit(shinyOptions(cache = origCache), add=TRUE)
p <- renderCachedPlot({ plot(1,1) }, { Sys.time() })
plt <- p(session, "name")
# Should have a size defined
expect_equal(plt$coordmap$dims$width, 692) #FIXME: why isn't this respecting the clientdata sizes?
expect_equal(plt$coordmap$dims$height, 400)
})
})
test_that("renderDataTable supported", {
session <- MockShinySession$new()

View File

@@ -659,115 +659,6 @@ test_that("suspended/resumed observers run at most once", {
})
test_that("reactive() accepts injected quosures", {
# Normal usage - no quosures
a <- 1
f <- reactive({ a + 10 })
a <- 2
expect_identical(isolate(f()), 12)
# quosures can be used in reactive()
a <- 1
f <- reactive({ rlang::eval_tidy(rlang::quo(!!a + 10)) })
a <- 2
expect_identical(isolate(f()), 12)
# inject() with quosures
a <- 1
exp <- rlang::quo(a + 10)
f <- inject(reactive(!!exp))
a <- 2
expect_identical(isolate(f()), 12)
# inject() with !!!
a <- 1
exp <- list(rlang::quo(a + 10))
f <- inject(reactive(!!!exp))
a <- 2
expect_identical(isolate(f()), 12)
# inject() with captured environment
a <- 1
exp <- local({
q <- rlang::quo(a + 10)
a <- 2
q
})
f <- inject(reactive(!! exp ))
a <- 3
expect_identical(isolate(f()), 12)
# inject() with nested quosures
a <- 1
y <- quo(a)
exp <- quo(!!y + 10)
a <- 2
f <- inject(reactive(!! exp ))
a <- 3
expect_identical(isolate(f()), 13)
})
test_that("observe() accepts injected quosures", {
# Normal usage - no quosures
val <- NULL
a <- 1
observe({ val <<- a + 10 })
a <- 2
flushReact()
expect_identical(val, 12)
# quosures can be used in reactive()
val <- NULL
a <- 1
f <- observe({ val <<- rlang::eval_tidy(rlang::quo(!!a + 10)) })
a <- 2
flushReact()
expect_identical(val, 12)
# inject() with quosures
val <- NULL
a <- 1
exp <- rlang::quo(val <<- a + 10)
f <- inject(observe(!!exp))
a <- 2
flushReact()
expect_identical(val, 12)
# inject() with !!!
val <- NULL
a <- 1
exp <- list(quo(val <<- a + 10))
f <- inject(observe(!!!exp))
a <- 2
flushReact()
expect_identical(val, 12)
# inject() with captured environment
val <- NULL
a <- 1
exp <- local({
q <- rlang::quo(val <<- a + 10)
a <- 2
q
})
f <- inject(observe(!! exp ))
a <- 3
flushReact()
expect_identical(val, 12)
# inject() with nested quosures
val <- NULL
a <- 1
y <- quo(a)
exp <- rlang::quo(val <<- !!y + 10)
a <- 2
f <- inject(observe(!!exp))
a <- 3
flushReact()
expect_identical(val, 13)
})
test_that("reactive() accepts quoted and unquoted expressions", {
vals <- reactiveValues(A=1)
@@ -793,12 +684,10 @@ test_that("reactive() accepts quoted and unquoted expressions", {
expect_true(is.function(isolate(fun())))
# Check that environment is correct - parent of parent environment should be
# this one. Note that rlang::as_function() injects an intermediate
# environment.
# Check that environment is correct - parent environment should be this one
this_env <- environment()
fun <- reactive(environment())
expect_identical(isolate(parent.env(parent.env(fun()))), this_env)
expect_identical(isolate(parent.env(fun())), this_env)
# Sanity check: environment structure for a reactive() should be the same as for
# a normal function
@@ -837,13 +726,12 @@ test_that("observe() accepts quoted and unquoted expressions", {
expect_equal(valB, 4)
# Check that environment is correct - parent of parent environment should be
# this one. rlang::as_function() injects one intermediate env.
# Check that environment is correct - parent environment should be this one
this_env <- environment()
inside_env <- NULL
fun <- observe(inside_env <<- environment())
flushReact()
expect_identical(parent.env(parent.env(inside_env)), this_env)
expect_identical(parent.env(inside_env), this_env)
})
test_that("Observer priorities are respected", {
@@ -1544,64 +1432,3 @@ test_that("reactiveTimer prefers session$scheduleTask", {
}
expect_gt(called, 0)
})
test_that("Reactive expression visibility", {
res <- NULL
rv <- reactive(1)
o <- observe({
res <<- withVisible(rv())
})
flushReact()
expect_identical(res, list(value = 1, visible = TRUE))
res <- NULL
rv <- reactive(invisible(1))
o <- observe({
res <<- withVisible(rv())
})
flushReact()
expect_identical(res, list(value = 1, visible = FALSE))
# isolate
expect_identical(
withVisible(isolate(1)),
list(value = 1, visible = TRUE)
)
expect_identical(
withVisible(isolate(invisible(1))),
list(value = 1, visible = FALSE)
)
})
test_that("Reactive expression labels", {
r <- list()
# Automatic label
r$x <- reactive({
a+1;b+ 2
})
# Printed output - uses expression, not `label`
expect_identical(
capture.output(print(r$x)),
c("reactive({", " a + 1", " b + 2", "}) ")
)
# Label used for debugging
expect_identical(
as.character(attr(r$x, "observable")$.label),
"r$x"
)
# With explicit label
r$y <- reactive({ a+1;b+ 2 }, label = "hello")
expect_identical(
capture.output(print(r$y)),
c("reactive({", " a + 1", " b + 2", "}) ")
)
expect_identical(
as.character(attr(r$y, "observable")$.label),
"hello"
)
})

View File

@@ -1,34 +0,0 @@
test_that("Render functions correctly handle quosures", {
# Normally, quosures are not unwrapped at creation time.
# However, using inject() will make it unwrap at creation time.
a <- 1
r1 <- inject(renderText({ !!a }))
r2 <- renderText({ eval_tidy(quo(!!a)) })
a <- 2
expect_identical(r1(), "1")
expect_identical(r2(), "2")
a <- 1
r1 <- inject(renderPrint({ !!a }))
r2 <- renderPrint({ eval_tidy(quo(!!a)) })
a <- 2
expect_identical(r1(), "[1] 1")
expect_identical(r2(), "[1] 2")
a <- 1
r1 <- inject(renderUI({ tags$p(!!a) }))
r2 <- renderUI({ eval_tidy(quo(tags$p(!!a))) })
a <- 2
res1 <- r1(shinysession = MockShinySession$new(), name = "foo")
expect_identical(as.character(res1$html), "<p>1</p>")
res2 <- r2(shinysession = MockShinySession$new(), name = "foo")
expect_identical(as.character(res2$html), "<p>2</p>")
a <- 1
r1 <- inject(renderTable({ pressure[!!a, ] }, digits = 1))
r2 <- renderTable({ eval_tidy(quo(pressure[!!a, ])) }, digits = 1)
a <- 2
expect_true(grepl("0\\.0", r1()))
expect_true(grepl("20\\.0", r2()))
})

View File

@@ -86,7 +86,7 @@ test_that("integration tests", {
"renderTable", "func", "force", "withVisible", "withCallingHandlers",
"domain$wrapSync", "promises::with_promise_domain",
"captureStackTraces", "doTryCatch", "tryCatchOne", "tryCatchList",
"tryCatch", "do", "hybrid_chain", "renderFunc", "renderTable({ C() }, server = FALSE)",
"tryCatch", "do", "hybrid_chain", "origRenderFunc", "renderTable({ C() }, server = FALSE)",
"..stacktraceon..", "contextFunc", "env$runWith", "force",
"domain$wrapSync", "promises::with_promise_domain",
"withReactiveDomain", "domain$wrapSync", "promises::with_promise_domain",

View File

@@ -566,7 +566,7 @@ test_that("validates server function", {
# bindings are considered `fields`.
get_mocked_publics <- function(instance, generator) {
publics <- ls(instance, all.names = TRUE)
actives <- names(generator$active) %||% character(0)
actives <- names(generator$active) %OR% character(0)
# Active bindings are considered fields.
methods_or_fields <- publics[!(publics %in% actives)]
methods <- character(0)

View File

@@ -165,47 +165,20 @@ test_that("app template works with runTests", {
random_folder <- paste0("shinyAppTemplate-", paste0(combo, collapse = "_"))
tempTemplateDir <- file.path(tempdir(), random_folder)
shinyAppTemplate(tempTemplateDir, combo)
on.exit(unlink(tempTemplateDir, recursive = TRUE), add = TRUE)
on.exit(unlink(tempTemplateDir, recursive = TRUE))
if (any(c("all", "shinytest", "testthat") %in% combo)) {
# suppress all output messages
ignore <- capture.output({
# do not let an error here stop this test
# string comparisons will be made below
test_result <- try({
runTests(tempTemplateDir)
}, silent = TRUE)
})
expected_test_output <- paste0(
c(
"Shiny App Test Results",
"* Success",
expect_output(
print(runTests(tempTemplateDir)),
paste0(
"Shiny App Test Results\\n\\* Success",
if (any(c("all", "shinytest") %in% combo))
paste0(" - ", file.path(random_folder, "tests", "shinytest.R")),
paste0("\\n - ", file.path(random_folder, "tests", "shinytest\\.R")),
if (any(c("all", "testthat") %in% combo))
paste0(" - ", file.path(random_folder, "tests", "testthat.R"))
),
collapse = "\n"
paste0("\\n - ", file.path(random_folder, "tests", "testthat\\.R"))
)
)
test_output <- paste0(
capture.output({
print(test_result)
}),
collapse = "\n"
)
if (identical(expected_test_output, test_output)) {
testthat::succeed()
} else {
# be very verbose in the error output to help find root cause of failure
cat("\nrunTests() output:\n", test_output, "\n\n")
cat("Expected print output:\n", expected_test_output, "\n")
testthat::fail(paste0("runTests() output for '", random_folder, "' failed. Received:\n", test_output, "\n"))
}
} else {
expect_error(
runTests(tempTemplateDir)

View File

@@ -205,39 +205,3 @@ test_that("Application directories are identified", {
normalizePath(dirname(tests), winslash = "/")
)
})
test_that("dateYMD works", {
expect_identical(dateYMD("2020-01-14"),"2020-01-14")
expect_identical(dateYMD("2020/01/14"),"2020-01-14")
expect_identical(
dateYMD(c("2020-01-14", "2019-11-05")),
c("2020-01-14", "2019-11-05")
)
expect_identical(
dateYMD(c("2020/01/14", "2019/11/05")),
c("2020-01-14", "2019-11-05")
)
expect_identical(
expect_warning(dateYMD("")),
""
)
expect_identical(
expect_warning(dateYMD(c(NA))),
NA
)
expect_identical(
expect_warning(dateYMD(c("", NA))),
c("", NA)
)
# If there are any bad values, the entire thing goes through unchanged
expect_identical(
expect_warning(dateYMD(c("2019/11/05", NA))),
c("2019/11/05", NA)
)
expect_identical(
expect_warning(dateYMD(c("2019/11/05", ""))),
c("2019/11/05", "")
)
})

View File

@@ -2,16 +2,16 @@ diff --git a/inst/www/shared/datepicker/scss/build3.scss b/inst/www/shared/datep
index b5388654..0dac6a62 100644
--- a/inst/www/shared/datepicker/scss/build3.scss
+++ b/inst/www/shared/datepicker/scss/build3.scss
@@ -6,32 +6,33 @@
@@ -6,32 +6,48 @@
//
// Variables and mixins copied from Bootstrap 3.3.5
-// Variables
-$gray: lighten(#000, 33.5%); // #555
-$gray-light: lighten(#000, 46.7%); // #777
-$gray-lighter: lighten(#000, 93.5%); // #eee
+// These are BS3 variables that are used in datepicker3.scss. So, when compiling against
+// a BS3 bslib theme, these variables should already be defined. Here we set
+// a BS3 bootstraplib theme, these variables should already be defined. Here we set
+// *defaults* for these variables based on BS4 variables, so this scss can work for
+// both BS3 and BS4
+$gray: mix($body-bg, $body-color, 33.5%) !default;
@@ -29,13 +29,21 @@ index b5388654..0dac6a62 100644
+//$line-height-base: 1.428571429;
+//$btn-link-disabled-color: $gray-light;
+//$dropdown-bg: #fff;
-$brand-primary: darken(#428bca, 6.5%); // #337ab7
+// Setup BS4 style color contrasting
+$yiq-contrasted-threshold: 150 !default;
+$yiq-text-dark: #212529 !default;
+$yiq-text-light: #fff !default;
-$btn-primary-color: #fff;
-$btn-primary-bg: $brand-primary;
-$btn-primary-border: darken($btn-primary-bg, 5%);
+@function color-yiq($color, $dark: $yiq-text-dark, $light: $yiq-text-light) {
+ $r: red($color);
+ $g: green($color);
+ $b: blue($color);
-$btn-link-disabled-color: $gray-light;
-
-$state-info-bg: #d9edf7;
@@ -45,12 +53,19 @@ index b5388654..0dac6a62 100644
-
-$dropdown-bg: #fff;
-$dropdown-border: rgba(0,0,0,.15);
+ $yiq: (($r * 299) + ($g * 587) + ($b * 114)) / 1000;
+ @if ($yiq >= $yiq-contrasted-threshold) {
+ @return $dark;
+ } @else {
+ @return $light;
+ }
+}
-// Mixins
+@mixin button-variant($background, $border) {
+ $color: color-contrast($background);
+ $color: color-yiq($background);
-// Button variants
-@mixin button-variant($color, $background, $border) {
color: $color;
@@ -79,7 +94,7 @@ index b5388654..0dac6a62 100644
- border-color: darken($border, 12%);
+ background-color: mix($background, $color, 90%);
+ border-color: mix($border, $color, 88%);
&:hover,
&:focus,
&.focus {
@@ -99,7 +114,7 @@ index 3d1621e0..3e031b31 100644
}
&.day:hover,
&.focused {
+ color: color-contrast($gray-lighter);
+ color: color-yiq($gray-lighter);
background: $gray-lighter;
cursor: pointer;
}
@@ -110,7 +125,7 @@ index 3d1621e0..3e031b31 100644
- @include button-variant(#000, $highlighted-bg, darken($highlighted-bg, 20%));
+ @include button-variant($highlighted-bg, darken($highlighted-bg, 20%));
border-radius: 0;
&.focused {
@@ -109,7 +110,7 @@
}
@@ -118,7 +133,7 @@ index 3d1621e0..3e031b31 100644
$today-bg: lighten(orange, 30%);
- @include button-variant(#000, $today-bg, darken($today-bg, 20%));
+ @include button-variant($today-bg, darken($today-bg, 20%));
&.focused {
background: darken($today-bg, 10%);
@@ -123,7 +124,7 @@
@@ -128,7 +143,7 @@ index 3d1621e0..3e031b31 100644
- @include button-variant(#000, $range-bg, darken($range-bg, 20%));
+ @include button-variant($range-bg, darken($range-bg, 20%));
border-radius: 0;
&.focused {
@@ -138,7 +139,7 @@
}
@@ -136,7 +151,7 @@ index 3d1621e0..3e031b31 100644
$range-highlighted-bg: mix($state-info-bg, $gray-lighter, 50%);
- @include button-variant(#000, $range-highlighted-bg, darken($range-highlighted-bg, 20%));
+ @include button-variant($range-highlighted-bg, darken($range-highlighted-bg, 20%));
&.focused {
background: darken($range-highlighted-bg, 10%);
@@ -152,7 +153,7 @@
@@ -145,7 +160,7 @@ index 3d1621e0..3e031b31 100644
$range-today-bg: mix(orange, $gray-lighter, 50%);
- @include button-variant(#000, $range-today-bg, darken($range-today-bg, 20%));
+ @include button-variant($range-today-bg, darken($range-today-bg, 20%));
&.disabled,
&.disabled:active {
@@ -162,12 +163,12 @@
@@ -167,7 +182,7 @@ index 3d1621e0..3e031b31 100644
border-radius: 4px;
&:hover,
&.focused {
+ color: color-contrast($gray-lighter);
+ color: color-yiq($gray-lighter);
background: $gray-lighter;
}
&.disabled,
@@ -185,8 +200,8 @@ index 3d1621e0..3e031b31 100644
cursor: pointer;
&:hover {
- background: $gray-lighter;
+ color: color-contrast($gray-lighter);
+ color: color-yiq($gray-lighter);
+ background: $gray-lighter;
}
}

View File

@@ -1,52 +0,0 @@
diff --git a/inst/www/shared/datepicker/scss/datepicker3.scss b/inst/www/shared/datepicker/scss/datepicker3.scss
index 3e031b31..758f7301 100644
--- a/inst/www/shared/datepicker/scss/datepicker3.scss
+++ b/inst/www/shared/datepicker/scss/datepicker3.scss
@@ -1,5 +1,15 @@
+// Both BS3 and BS4 define a border radius mixin, but just in case
+// we're trying to compile this without bootstrapSass
+@mixin border-radius-shim($radius) {
+ @if mixin-exists("border-radius") {
+ @include border-radius($radius);
+ } @else {
+ border-radius: $radius;
+ }
+}
+
.datepicker {
- border-radius: $border-radius-base;
+ @include border-radius-shim($border-radius-base);
&-inline {
width: 220px;
}
@@ -64,7 +74,7 @@
text-align: center;
width: 30px;
height: 30px;
- border-radius: 4px;
+ @include border-radius-shim(4px);
border: none;
}
}
@@ -179,7 +189,7 @@
float: left;
margin: 1%;
cursor: pointer;
- border-radius: 4px;
+ @include border-radius-shim(4px);
&:hover,
&.focused {
color: color-contrast($gray-lighter);
@@ -243,10 +253,10 @@
text-align: center;
}
input:first-child {
- border-radius: 3px 0 0 3px;
+ @include border-radius-shim(3px 0 0 3px);
}
input:last-child {
- border-radius: 0 3px 3px 0;
+ @include border-radius-shim(0 3px 3px 0);
}
.input-group-addon {
width: auto;

View File

@@ -32,8 +32,7 @@ local({
known_unindexed <- c("shiny-package", "stacktrace", "knitr_methods",
"pageWithSidebar", "headerPanel", "shiny.appobj",
"deprecatedReactives", "reexports", "makeReactiveBinding",
"reactiveConsole", "registerThemeDependency",
"memoryCache", "diskCache")
"reactiveConsole")
## This test ensures that every documented topic is included in
## staticdocs/index.r, unless explicitly waived by specifying it
@@ -43,15 +42,12 @@ local({
unknown <- setdiff(c(known_unindexed, indexed_topics), c(all_topics, reexports_man_file_names))
testthat::expect_equal(length(missing), 0,
info = paste("Functions missing from ./tools/documentation/pkgdown.yml:\n",
paste(" - ", missing, sep = "", collapse = "\n"),
"\nPlease update ./tools/documentation/pkgdown.yml or ",
"`known_unindexed` in ./tools/documentation/checkPkgdown.R",
info = paste("Functions missing from _pkgdown.yml:\n",
paste(" ", missing, sep = "", collapse = "\n"),
sep = ""))
testthat::expect_equal(length(unknown), 0,
info = paste("Unrecognized functions in ./tools/documentation/pkgdown.yml:\n",
paste(" - ", unknown, sep = "", collapse = "\n"),
"\nPlease update ./tools/documentation/pkgdown.yml",
info = paste("Unrecognized functions in _pkgdown.yml:\n",
paste(" ", unknown, sep = "", collapse = "\n"),
sep = ""))
invisible(TRUE)
})

View File

@@ -118,8 +118,6 @@ reference:
- observeEvent
- reactiveVal
- reactiveValues
- bindCache
- bindEvent
- reactiveValuesToList
- is.reactivevalues
- isolate
@@ -178,7 +176,6 @@ reference:
- applyInputHandlers
- exprToFunction
- installExprFunction
- quoToFunction
- parseQueryString
- getCurrentOutputInfo
- getCurrentTheme
@@ -194,6 +191,8 @@ reference:
- shinyDeprecated
- serverInfo
- onStop
- diskCache
- memoryCache
- httpResponse
- key_missing
- title: Plot interaction

View File

@@ -1,9 +1,9 @@
diff --git a/inst/www/shared/ionrangeslider/scss/shiny.scss b/inst/www/shared/ionrangeslider/scss/shiny.scss
new file mode 100644
index 00000000..4a0fccc1
index 00000000..3356cb6d
--- /dev/null
+++ b/inst/www/shared/ionrangeslider/scss/shiny.scss
@@ -0,0 +1,174 @@
@@ -0,0 +1,182 @@
+/* 'shiny' skin for Ion.RangeSlider, largely based on the 'big' skin, but with smaller dimensions, grayscale grid text, and without gradients
+© RStudio, Inc, 2014
+© Denis Ineshin, 2014 https://github.com/IonDen
@@ -12,17 +12,25 @@ index 00000000..4a0fccc1
+
+@import "_base";
+
+// Both BS3 and BS4 define a border radius mixin, but just in case
+// we're trying to compile this without bootstrapSass
+@mixin border-radius-shim($radius) {
+ @if mixin-exists("border-radius") {
+ @include border-radius($radius);
+////////////////////////////////////////////////////////////////////////////
+// Setup BS4 style color contrasting
+$yiq-contrasted-threshold: 150 !default;
+$yiq-text-dark: #212529 !default;
+$yiq-text-light: #fff !default;
+
+@function color-yiq($color, $dark: $yiq-text-dark, $light: $yiq-text-light) {
+ $r: red($color);
+ $g: green($color);
+ $b: blue($color);
+
+ $yiq: (($r * 299) + ($g * 587) + ($b * 114)) / 1000;
+
+ @if ($yiq >= $yiq-contrasted-threshold) {
+ @return $dark;
+ } @else {
+ border-radius: $radius;
+ @return $light;
+ }
+}
+
+
+////////////////////////////////////////////////////////////////////////////
+
+// Re-define font-family on .irs to make it configurable
@@ -64,7 +72,7 @@ index 00000000..4a0fccc1
+ $minmax_line_height: 1.333 !default;
+
+ $fromto_bg_color: $accent !default;
+ $fromto_color: color-contrast($fromto_bg_color) !default;
+ $fromto_color: color-yiq($fromto_bg_color) !default;
+ $fromto_font_size: 11px !default;
+ $fromto_line_height: 1.333 !default;
+
@@ -85,7 +93,7 @@ index 00000000..4a0fccc1
+ background: $line_bg;
+ background-color: $line_bg_color;
+ border: $line_border;
+ @include border-radius-shim($line_height);
+ border-radius: $line_height;
+ }
+
+ .#{$name}-bar {
@@ -96,7 +104,7 @@ index 00000000..4a0fccc1
+ background: $bar_color;
+
+ &--single {
+ @include border-radius-shim($line_height 0 0 $line_height);
+ border-radius: $line_height 0 0 $line_height;
+ }
+ }
+
@@ -105,7 +113,7 @@ index 00000000..4a0fccc1
+ top: 38px;
+ height: 2px;
+ background: rgba($fg, 0.3);
+ @include border-radius-shim(5px);
+ border-radius: 5px;
+ }
+ .lt-ie9 .#{$name}-shadow {
+ filter: alpha(opacity=30);
@@ -120,7 +128,7 @@ index 00000000..4a0fccc1
+ border: $handle_border;
+ background-color: $handle_color;
+ box-shadow: $handle_box_shadow;
+ @include border-radius-shim($handle_width);
+ border-radius: $handle_width;
+
+ &.state_hover,
+ &:hover {
@@ -135,7 +143,7 @@ index 00000000..4a0fccc1
+ color: $minmax_text_color;
+ text-shadow: none;
+ background-color: $minmax_bg_color;
+ @include border-radius-shim($custom_radius);
+ border-radius: $custom_radius;
+ font-size: $minmax_font_size;
+ line-height: $minmax_line_height;
+ }
@@ -152,7 +160,7 @@ index 00000000..4a0fccc1
+ text-shadow: none;
+ padding: 1px 3px;
+ background-color: $fromto_bg_color;
+ @include border-radius-shim($custom_radius);
+ border-radius: $custom_radius;
+ font-size: $fromto_font_size;
+ line-height: $fromto_line_height;
+ }

View File

@@ -1,19 +0,0 @@
diff --git a/inst/www/shared/selectize/scss/selectize.scss b/inst/www/shared/selectize/scss/selectize.scss
index 240e3895..c20f529c 100644
--- a/inst/www/shared/selectize/scss/selectize.scss
+++ b/inst/www/shared/selectize/scss/selectize.scss
@@ -57,9 +57,11 @@ $selectize-caret-margin: 0 2px 0 0 !default;
$selectize-caret-margin-rtl: 0 4px 0 -2px !default;
@mixin selectize-border-radius($radii){
- -webkit-border-radius: $radii;
- -moz-border-radius: $radii;
- border-radius: $radii;
+ @if mixin-exists("border-radius") {
+ @include border-radius($radii)
+ } @else {
+ border-radius: $radii;
+ }
}
@mixin selectize-unselectable(){
-webkit-user-select: none;

View File

@@ -75,19 +75,17 @@ for (patch in list.files(patch_dir, full.names = TRUE)) {
# Compile to CSS
library(sass)
library(bslib)
library(bootstraplib)
css_dir <- file.path(dest_dir, "css")
dir.create(css_dir, recursive = TRUE)
sass_partial(
bs_sass(
sass_file(file.path(dest_dir, "scss", "build3.scss")),
bundle = bs_theme(),
output = file.path(css_dir, "bootstrap-datepicker3.css"),
write_attachments = FALSE
theme = bs_theme(),
output = file.path(css_dir, "bootstrap-datepicker3.css")
)
sass_partial(
bs_sass(
sass_file(file.path(dest_dir, "scss", "build3.scss")),
bundle = bs_theme(),
theme = bs_theme(),
output = file.path(css_dir, "bootstrap-datepicker3.min.css"),
options = sass_options(output_style = "compressed"),
write_attachments = FALSE
options = sass_options(output_style = "compressed")
)

View File

@@ -57,10 +57,7 @@ for (patch in list.files(patch_dir, full.names = TRUE)) {
library(sass)
dir.create(file.path(target, "css"))
sass(
list(
sass::sass_file(system.file("sass-utils/color-contrast.scss", package = "bslib")),
sass_file(file.path(target, "scss", "shiny.scss"))
),
sass_file(file.path(target, "scss", "shiny.scss")),
output = file.path(target, "css", "ion.rangeSlider.css")
)