mirror of
https://github.com/rstudio/shiny.git
synced 2026-01-12 16:38:06 -05:00
Compare commits
1 Commits
async-load
...
update-new
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fba4680734 |
10
NEWS.md
10
NEWS.md
@@ -5,8 +5,6 @@ shiny development
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Closed #3626: `renderPlot()` (and `plotPNG()`) now uses `ragg::agg_png()` by default when the [`{ragg}` package](https://github.com/r-lib/ragg) is installed. To restore the previous behavior, set `options(shiny.useragg = FALSE)`. (#3654)
|
||||
|
||||
### Minor new features and improvements
|
||||
|
||||
* Shiny's internal HTML dependencies are now mounted dynamically instead of statically. (#3537)
|
||||
@@ -23,8 +21,6 @@ shiny development
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Closed #3657: `throttle.ts` and the `Throttler` typescript objects it provides now function as intended.
|
||||
|
||||
* Closed tidyverse/dplyr#5552: Compatibility of dplyr 1.0 (and rlang chained errors in general) with `req()`, `validate()`, and friends.
|
||||
|
||||
* Closed #1545: `insertUI()` now executes `<script>` tags. (#3630)
|
||||
@@ -43,11 +39,7 @@ shiny development
|
||||
|
||||
* `fileInput()` can set the `capture` attribute to facilitates user access to a device's media capture mechanism, such as a camera, or microphone, from within a file upload control ([W3C HTML Media Capture](https://www.w3.org/TR/html-media-capture/)). (Thanks to khaled-alshamaa, #3481)
|
||||
|
||||
* Closed rstudio/shinytest2#222: When restoring a context (i.e., bookmarking) from a URL, Shiny now better handles a trailing `=` after `_inputs_` and `_values_`. (#3648)
|
||||
|
||||
* Closed #3581: Errors in throttled/debounced reactive expressions no longer cause the session to exit. (#3624)
|
||||
|
||||
* Closed #3250:`{rlang}`/`{tidyeval}` conditions (i.e., warnings and errors) are no longer filtered from stack traces. (#3602)
|
||||
* Fixed rstudio/shinytest2#222: When restoring a context (i.e., bookmarking) from a URL, Shiny now better handles a trailing `=` after `_inputs_` and `_values_`. (#3648)
|
||||
|
||||
|
||||
shiny 1.7.1
|
||||
|
||||
@@ -370,7 +370,7 @@ RestoreContext <- R6Class("RestoreContext",
|
||||
safeFromJSON(value),
|
||||
error = function(e) {
|
||||
varsUnparsed <<- c(varsUnparsed, name)
|
||||
warning("Failed to parse URL parameter \"", name, "\"")
|
||||
message("Failed to parse URL parameter \"", name, "\"")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -421,17 +421,8 @@ pruneStackTrace <- function(parents) {
|
||||
# Loop over the parent indices. Anything that is not parented by current_node
|
||||
# (a.k.a. last-known-good node), or is a dupe, can be discarded. Anything that
|
||||
# is kept becomes the new current_node.
|
||||
#
|
||||
# jcheng 2022-03-18: Two more reasons a node can be kept:
|
||||
# 1. parent is 0
|
||||
# 2. parent is i
|
||||
# Not sure why either of these situations happen, but they're common when
|
||||
# interacting with rlang/dplyr errors. See issue rstudio/shiny#3250 for repro
|
||||
# cases.
|
||||
include <- vapply(seq_along(parents), function(i) {
|
||||
if ((!is_dupe[[i]] && parents[[i]] == current_node) ||
|
||||
parents[[i]] == 0 ||
|
||||
parents[[i]] == i) {
|
||||
if (!is_dupe[[i]] && parents[[i]] == current_node) {
|
||||
current_node <<- i
|
||||
TRUE
|
||||
} else {
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
startPNG <- function(filename, width, height, res, ...) {
|
||||
pngfun <- if ((getOption('shiny.useragg') %||% TRUE) && is_installed("ragg")) {
|
||||
ragg::agg_png
|
||||
# shiny.useragg is an experimental option that isn't officially supported or
|
||||
# documented. It's here in the off chance that someone really wants
|
||||
# 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_installed("ragg")) {
|
||||
pngfun <- ragg::agg_png
|
||||
} else if (capabilities("aqua")) {
|
||||
# i.e., png(type = 'quartz')
|
||||
grDevices::png
|
||||
pngfun <- grDevices::png
|
||||
} else if ((getOption('shiny.usecairo') %||% TRUE) && is_installed("Cairo")) {
|
||||
Cairo::CairoPNG
|
||||
pngfun <- Cairo::CairoPNG
|
||||
} else {
|
||||
# i.e., png(type = 'cairo')
|
||||
grDevices::png
|
||||
pngfun <- grDevices::png
|
||||
}
|
||||
|
||||
args <- rlang::list2(filename=filename, width=width, height=height, res=res, ...)
|
||||
@@ -52,31 +57,33 @@ startPNG <- function(filename, width, height, res, ...) {
|
||||
grDevices::dev.cur()
|
||||
}
|
||||
|
||||
#' Capture a plot as a PNG file.
|
||||
#' Run a plotting function and save the output as a PNG
|
||||
#'
|
||||
#' The PNG graphics device used is determined in the following order:
|
||||
#' * If the ragg package is installed (and the `shiny.useragg` is not
|
||||
#' set to `FALSE`), then use [ragg::agg_png()].
|
||||
#' * If a quartz device is available (i.e., `capabilities("aqua")` is
|
||||
#' `TRUE`), then use `png(type = "quartz")`.
|
||||
#' * If the Cairo package is installed (and the `shiny.usecairo` option
|
||||
#' is not set to `FALSE`), then use [Cairo::CairoPNG()].
|
||||
#' * Otherwise, use [grDevices::png()]. In this case, Linux and Windows
|
||||
#' may not antialias some point shapes, resulting in poor quality output.
|
||||
#' This function returns the name of the PNG file that it generates. In
|
||||
#' essence, it calls `png()`, then `func()`, then `dev.off()`.
|
||||
#' So `func` must be a function that will generate a plot when used this
|
||||
#' way.
|
||||
#'
|
||||
#' For output, it will try to use the following devices, in this order:
|
||||
#' quartz (via [grDevices::png()]), then [Cairo::CairoPNG()],
|
||||
#' and finally [grDevices::png()]. This is in order of quality of
|
||||
#' output. Notably, plain `png` output on Linux and Windows may not
|
||||
#' antialias some point shapes, resulting in poor quality output.
|
||||
#'
|
||||
#' In some cases, `Cairo()` provides output that looks worse than
|
||||
#' `png()`. To disable Cairo output for an app, use
|
||||
#' `options(shiny.usecairo=FALSE)`.
|
||||
#'
|
||||
#' @param func A function that generates a plot.
|
||||
#' @param filename The name of the output file. Defaults to a temp file with
|
||||
#' extension `.png`.
|
||||
#' @param width Width in pixels.
|
||||
#' @param height Height in pixels.
|
||||
#' @param res Resolution in pixels per inch. This value is passed to the
|
||||
#' graphics device. Note that this affects the resolution of PNG rendering in
|
||||
#' @param res Resolution in pixels per inch. This value is passed to
|
||||
#' [grDevices::png()]. Note that this affects the resolution of PNG rendering in
|
||||
#' R; it won't change the actual ppi of the browser.
|
||||
#' @param ... Arguments to be passed through to the graphics device. These can
|
||||
#' be used to set the width, height, background color, etc.
|
||||
#'
|
||||
#' @return A path to the newly generated PNG file.
|
||||
#'
|
||||
#' @param ... Arguments to be passed through to [grDevices::png()].
|
||||
#' These can be used to set the width, height, background color, etc.
|
||||
#' @export
|
||||
plotPNG <- function(func, filename=tempfile(fileext='.png'),
|
||||
width=400, height=400, res=72, ...) {
|
||||
|
||||
@@ -2472,11 +2472,11 @@ debounce <- function(r, millis, priority = 100, domain = getDefaultReactiveDomai
|
||||
|
||||
# Ensure r() is called only after setting firstRun to FALSE since r()
|
||||
# may throw an error
|
||||
try(r(), silent = TRUE)
|
||||
r()
|
||||
return()
|
||||
}
|
||||
# This ensures r() is still tracked after firstRun
|
||||
try(r(), silent = TRUE)
|
||||
r()
|
||||
|
||||
# The value (or possibly millis) changed. Start or reset the timer.
|
||||
v$when <- getDomainTimeMs(domain) + millis()
|
||||
@@ -2509,7 +2509,7 @@ debounce <- function(r, millis, priority = 100, domain = getDefaultReactiveDomai
|
||||
# commenting it out and studying the unit test failure that results.
|
||||
primer <- observe({
|
||||
primer$destroy()
|
||||
try(er(), silent = TRUE)
|
||||
er()
|
||||
}, label = "debounce primer", domain = domain, priority = priority)
|
||||
|
||||
er
|
||||
@@ -2551,7 +2551,7 @@ throttle <- function(r, millis, priority = 100, domain = getDefaultReactiveDomai
|
||||
}
|
||||
|
||||
# Responsible for tracking when f() changes.
|
||||
observeEvent(try(r(), silent = TRUE), {
|
||||
observeEvent(r(), {
|
||||
if (v$pending) {
|
||||
# In a blackout period and someone already scheduled; do nothing
|
||||
} else if (blackoutMillisLeft() > 0) {
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
#' When rendering an inline plot, you must provide numeric values (in pixels)
|
||||
#' to both \code{width} and \code{height}.
|
||||
#' @param res Resolution of resulting plot, in pixels per inch. This value is
|
||||
#' passed to [plotPNG()]. Note that this affects the resolution of PNG
|
||||
#' passed to [grDevices::png()]. Note that this affects the resolution of PNG
|
||||
#' rendering in R; it won't change the actual ppi of the browser.
|
||||
#' @param alt Alternate text for the HTML `<img>` tag if it cannot be displayed
|
||||
#' or viewed (i.e., the user uses a screen reader). In addition to a character
|
||||
@@ -44,7 +44,7 @@
|
||||
#' ggplot objects; for other plots, `NA` results in alt text of "Plot object".
|
||||
#' `NULL` or `""` is not recommended because those should be limited to
|
||||
#' decorative images.
|
||||
#' @param ... Arguments to be passed through to [plotPNG()].
|
||||
#' @param ... Arguments to be passed through to [grDevices::png()].
|
||||
#' These can be used to set the width, height, background color, etc.
|
||||
#' @inheritParams renderUI
|
||||
#' @param execOnResize If `FALSE` (the default), then when a plot is
|
||||
|
||||
@@ -140,10 +140,9 @@ getShinyOption <- function(name, default = NULL) {
|
||||
#' messages).}
|
||||
#' \item{shiny.autoload.r (defaults to `TRUE`)}{If `TRUE`, then the R/
|
||||
#' of a shiny app will automatically be sourced.}
|
||||
#' \item{shiny.useragg (defaults to `TRUE`)}{Set to `FALSE` to prevent PNG rendering via the
|
||||
#' ragg package. See [plotPNG()] for more information.}
|
||||
#' \item{shiny.usecairo (defaults to `TRUE`)}{Set to `FALSE` to prevent PNG rendering via the
|
||||
#' Cairo package. See [plotPNG()] for more information.}
|
||||
#' \item{shiny.usecairo (defaults to `TRUE`)}{This is used to disable graphical rendering by the
|
||||
#' Cairo package, if it is installed. See [plotPNG()] for more
|
||||
#' information.}
|
||||
#' \item{shiny.devmode (defaults to `NULL`)}{Option to enable Shiny Developer Mode. When set,
|
||||
#' different default `getOption(key)` values will be returned. See [devmode()] for more details.}
|
||||
### Not documenting as 'shiny.devmode.verbose' is for niche use only
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
2
inst/www/shared/shiny.min.js
vendored
2
inst/www/shared/shiny.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -2,7 +2,7 @@
|
||||
% Please edit documentation in R/imageutils.R
|
||||
\name{plotPNG}
|
||||
\alias{plotPNG}
|
||||
\title{Capture a plot as a PNG file.}
|
||||
\title{Run a plotting function and save the output as a PNG}
|
||||
\usage{
|
||||
plotPNG(
|
||||
func,
|
||||
@@ -23,26 +23,27 @@ extension \code{.png}.}
|
||||
|
||||
\item{height}{Height in pixels.}
|
||||
|
||||
\item{res}{Resolution in pixels per inch. This value is passed to the
|
||||
graphics device. Note that this affects the resolution of PNG rendering in
|
||||
\item{res}{Resolution in pixels per inch. This value is passed to
|
||||
\code{\link[grDevices:png]{grDevices::png()}}. Note that this affects the resolution of PNG rendering in
|
||||
R; it won't change the actual ppi of the browser.}
|
||||
|
||||
\item{...}{Arguments to be passed through to the graphics device. These can
|
||||
be used to set the width, height, background color, etc.}
|
||||
}
|
||||
\value{
|
||||
A path to the newly generated PNG file.
|
||||
\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.}
|
||||
}
|
||||
\description{
|
||||
The PNG graphics device used is determined in the following order:
|
||||
\itemize{
|
||||
\item If the ragg package is installed (and the \code{shiny.useragg} is not
|
||||
set to \code{FALSE}), then use \code{\link[ragg:agg_png]{ragg::agg_png()}}.
|
||||
\item If a quartz device is available (i.e., \code{capabilities("aqua")} is
|
||||
\code{TRUE}), then use \code{png(type = "quartz")}.
|
||||
\item If the Cairo package is installed (and the \code{shiny.usecairo} option
|
||||
is not set to \code{FALSE}), then use \code{\link[Cairo:Cairo]{Cairo::CairoPNG()}}.
|
||||
\item Otherwise, use \code{\link[grDevices:png]{grDevices::png()}}. In this case, Linux and Windows
|
||||
may not antialias some point shapes, resulting in poor quality output.
|
||||
This function returns the name of the PNG file that it generates. In
|
||||
essence, it calls \code{png()}, then \code{func()}, then \code{dev.off()}.
|
||||
So \code{func} must be a function that will generate a plot when used this
|
||||
way.
|
||||
}
|
||||
\details{
|
||||
For output, it will try to use the following devices, in this order:
|
||||
quartz (via \code{\link[grDevices:png]{grDevices::png()}}), then \code{\link[Cairo:Cairo]{Cairo::CairoPNG()}},
|
||||
and finally \code{\link[grDevices:png]{grDevices::png()}}. This is in order of quality of
|
||||
output. Notably, plain \code{png} output on Linux and Windows may not
|
||||
antialias some point shapes, resulting in poor quality output.
|
||||
|
||||
In some cases, \code{Cairo()} provides output that looks worse than
|
||||
\code{png()}. To disable Cairo output for an app, use
|
||||
\code{options(shiny.usecairo=FALSE)}.
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ information on the default sizing policy.}
|
||||
(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{...}{Arguments to be passed through to \code{\link[=plotPNG]{plotPNG()}}.
|
||||
\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.}
|
||||
|
||||
\item{alt}{Alternate text for the HTML \verb{<img>} tag if it cannot be displayed
|
||||
|
||||
@@ -35,10 +35,10 @@ When rendering an inline plot, you must provide numeric values (in pixels)
|
||||
to both \code{width} and \code{height}.}
|
||||
|
||||
\item{res}{Resolution of resulting plot, in pixels per inch. This value is
|
||||
passed to \code{\link[=plotPNG]{plotPNG()}}. Note that this affects the resolution of PNG
|
||||
passed to \code{\link[grDevices:png]{grDevices::png()}}. Note that this affects the resolution of PNG
|
||||
rendering in R; it won't change the actual ppi of the browser.}
|
||||
|
||||
\item{...}{Arguments to be passed through to \code{\link[=plotPNG]{plotPNG()}}.
|
||||
\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.}
|
||||
|
||||
\item{alt}{Alternate text for the HTML \verb{<img>} tag if it cannot be displayed
|
||||
|
||||
@@ -119,10 +119,9 @@ values are \code{"send"} (only print messages sent to the client),
|
||||
messages).}
|
||||
\item{shiny.autoload.r (defaults to \code{TRUE})}{If \code{TRUE}, then the R/
|
||||
of a shiny app will automatically be sourced.}
|
||||
\item{shiny.useragg (defaults to \code{TRUE})}{Set to \code{FALSE} to prevent PNG rendering via the
|
||||
ragg package. See \code{\link[=plotPNG]{plotPNG()}} for more information.}
|
||||
\item{shiny.usecairo (defaults to \code{TRUE})}{Set to \code{FALSE} to prevent PNG rendering via the
|
||||
Cairo package. See \code{\link[=plotPNG]{plotPNG()}} for more information.}
|
||||
\item{shiny.usecairo (defaults to \code{TRUE})}{This is used to disable graphical rendering by the
|
||||
Cairo package, if it is installed. See \code{\link[=plotPNG]{plotPNG()}} for more
|
||||
information.}
|
||||
\item{shiny.devmode (defaults to \code{NULL})}{Option to enable Shiny Developer Mode. When set,
|
||||
different default \code{getOption(key)} values will be returned. See \code{\link[=devmode]{devmode()}} for more details.}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { mergeSort } from "../utils";
|
||||
import { Callbacks } from "../utils/callbacks";
|
||||
|
||||
interface BindingBase {
|
||||
name: string;
|
||||
@@ -15,7 +14,6 @@ class BindingRegistry<Binding extends BindingBase> {
|
||||
name: string;
|
||||
bindings: Array<BindingObj<Binding>> = [];
|
||||
bindingNames: { [key: string]: BindingObj<Binding> } = {};
|
||||
registerCallbacks: Callbacks = new Callbacks();
|
||||
|
||||
register(binding: Binding, bindingName: string, priority = 0): void {
|
||||
const bindingObj = { binding, priority };
|
||||
@@ -25,12 +23,6 @@ class BindingRegistry<Binding extends BindingBase> {
|
||||
this.bindingNames[bindingName] = bindingObj;
|
||||
binding.name = bindingName;
|
||||
}
|
||||
|
||||
this.registerCallbacks.invoke();
|
||||
}
|
||||
|
||||
onRegister(fn: () => void, once = true): void {
|
||||
this.registerCallbacks.register(fn, once);
|
||||
}
|
||||
|
||||
setPriority(bindingName: string, priority: number): void {
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
import { bindAll, unbindAll, _bindAll } from "./bind";
|
||||
import type { BindInputsCtx, BindScope } from "./bind";
|
||||
import { setShinyObj } from "./initedMethods";
|
||||
import { registerDependency, renderHtml } from "./render";
|
||||
import { registerDependency } from "./render";
|
||||
import { sendImageSizeFns } from "./sendImageSize";
|
||||
import { ShinyApp } from "./shinyapp";
|
||||
import { registerNames as singletonsRegisterNames } from "./singletons";
|
||||
@@ -150,19 +150,6 @@ function initShiny(windowShiny: Shiny): void {
|
||||
(x) => x.value
|
||||
);
|
||||
|
||||
// When future bindings are registered via dynamic UI, check to see if renderHtml()
|
||||
// is currently executing. If it's not, it's likely that the binding registration
|
||||
// is occurring a tick after renderHtml()/renderContent(), in which case we need
|
||||
// to make sure the new bindings get a chance to bind to the DOM. (#3635)
|
||||
const maybeBindOnRegister = debounce(0, () => {
|
||||
if (!renderHtml.isExecuting()) {
|
||||
windowShiny.bindAll(document.documentElement);
|
||||
}
|
||||
});
|
||||
|
||||
inputBindings.onRegister(maybeBindOnRegister, false);
|
||||
outputBindings.onRegister(maybeBindOnRegister, false);
|
||||
|
||||
// The server needs to know the size of each image and plot output element,
|
||||
// in case it is auto-sizing
|
||||
$(".shiny-image-output, .shiny-plot-output, .shiny-report-size").each(
|
||||
|
||||
@@ -72,20 +72,10 @@ function renderHtml(
|
||||
dependencies: HtmlDep[],
|
||||
where: WherePosition = "replace"
|
||||
): ReturnType<typeof singletonsRenderHtml> {
|
||||
renderHtml._renderCount++;
|
||||
try {
|
||||
renderDependencies(dependencies);
|
||||
return singletonsRenderHtml(html, el, where);
|
||||
} finally {
|
||||
renderHtml._renderCount--;
|
||||
}
|
||||
renderDependencies(dependencies);
|
||||
return singletonsRenderHtml(html, el, where);
|
||||
}
|
||||
|
||||
renderHtml._renderCount = 0;
|
||||
renderHtml.isExecuting = function () {
|
||||
return renderHtml._renderCount > 0;
|
||||
};
|
||||
|
||||
type HtmlDepVersion = string;
|
||||
|
||||
type MetaItem = {
|
||||
@@ -209,9 +199,6 @@ function renderDependency(dep_: HtmlDep) {
|
||||
$head.append(stylesheetLinks);
|
||||
}
|
||||
|
||||
const scriptPromises: Array<Promise<any>> = [];
|
||||
const scriptElements: HTMLScriptElement[] = [];
|
||||
|
||||
dep.script.forEach((x) => {
|
||||
const script = document.createElement("script");
|
||||
|
||||
@@ -223,23 +210,9 @@ function renderDependency(dep_: HtmlDep) {
|
||||
script.setAttribute(attr, val ? val : "");
|
||||
});
|
||||
|
||||
const p = new Promise((resolve) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
script.onload = (e: Event) => {
|
||||
resolve(null);
|
||||
};
|
||||
});
|
||||
|
||||
scriptPromises.push(p);
|
||||
scriptElements.push(script);
|
||||
$head.append(script);
|
||||
});
|
||||
|
||||
// Append the script elements all at once, so that we're sure they'll load in
|
||||
// order. (We didn't append them individually in the `forEach()` above,
|
||||
// because we're not sure that the browser will load them in order if done
|
||||
// that way.)
|
||||
document.head.append(...scriptElements);
|
||||
|
||||
dep.attachment.forEach((x) => {
|
||||
const link = $("<link rel='attachment'>")
|
||||
.attr("id", dep.name + "-" + x.key + "-attachment")
|
||||
@@ -248,22 +221,12 @@ function renderDependency(dep_: HtmlDep) {
|
||||
$head.append(link);
|
||||
});
|
||||
|
||||
Promise.allSettled(scriptPromises).then(() => {
|
||||
// After the scripts are all loaded, insert any head content. This may
|
||||
// contain <script> tags with inline content, which we want to execute after
|
||||
// the script elements above, because the code here may depend on them.
|
||||
if (dep.head) {
|
||||
const $newHead = $("<head></head>");
|
||||
|
||||
$newHead.html(dep.head);
|
||||
$head.append($newHead.children());
|
||||
}
|
||||
|
||||
// Bind all
|
||||
shinyInitializeInputs(document.body);
|
||||
shinyBindAll(document.body);
|
||||
});
|
||||
if (dep.head) {
|
||||
const $newHead = $("<head></head>");
|
||||
|
||||
$newHead.html(dep.head);
|
||||
$head.append($newHead.children());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,67 +19,40 @@ class Throttler<X extends AnyVoidFunction> implements InputRatePolicy<X> {
|
||||
this.args = null;
|
||||
}
|
||||
|
||||
// If no timer is currently running, immediately call the function and set the
|
||||
// timer; if a timer is running out, just queue up the args for the call when
|
||||
// the timer runs out. Later calls during the same timeout will overwrite
|
||||
// earlier ones.
|
||||
normalCall(...args: Parameters<X>): void {
|
||||
// This will be an empty array (not null) if called without arguments, and
|
||||
// `[null]` if called with `null`.
|
||||
this.args = args;
|
||||
|
||||
// Only invoke immediately if there isn't a timer running.
|
||||
if (this.timerId === null) {
|
||||
this.$invoke();
|
||||
this.timerId = setTimeout(() => {
|
||||
// IE8 doesn't reliably clear timeout, so this additional
|
||||
// check is needed
|
||||
if (this.timerId === null) return;
|
||||
this.$clearTimer();
|
||||
if (args.length > 0) this.normalCall(...args);
|
||||
}, this.delayMs);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset the timer if active and call immediately
|
||||
immediateCall(...args: Parameters<X>): void {
|
||||
this.$clearTimer();
|
||||
this.args = args;
|
||||
this.$invoke();
|
||||
}
|
||||
|
||||
// Is there a call waiting to send?
|
||||
isPending(): boolean {
|
||||
return this.args !== null;
|
||||
return this.timerId !== null;
|
||||
}
|
||||
|
||||
$clearTimer(): void {
|
||||
if (this.timerId !== null) {
|
||||
clearTimeout(this.timerId);
|
||||
this.timerId = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Invoke the throttled function with the currently-stored args and start the
|
||||
// timer.
|
||||
$invoke(): void {
|
||||
if (this.args === null) {
|
||||
// Shouldn't get here, because $invoke should only be called right after
|
||||
// setting this.args. But just in case.
|
||||
return;
|
||||
if (this.args && this.args.length > 0) {
|
||||
this.func.apply(this.target, this.args);
|
||||
} else {
|
||||
this.func.apply(this.target);
|
||||
}
|
||||
|
||||
this.func.apply(this.target, this.args);
|
||||
|
||||
// Clear the stored args. This is used to track if a call is pending.
|
||||
this.args = null;
|
||||
|
||||
// Set this.timerId to a newly-created timer, which will invoke a call with
|
||||
// the most recently called args (if any) when it expires.
|
||||
this.timerId = setTimeout(() => {
|
||||
// IE8 doesn't reliably clear timeout, so this additional check is needed
|
||||
if (this.timerId === null) return;
|
||||
|
||||
this.$clearTimer();
|
||||
// Do we have a call queued up?
|
||||
if (this.isPending()) {
|
||||
// If so, invoke the call with queued args and reset timer.
|
||||
this.$invoke();
|
||||
}
|
||||
}, this.delayMs);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
type Cb = {
|
||||
once: boolean;
|
||||
fn: () => void;
|
||||
};
|
||||
|
||||
type Cbs = {
|
||||
[key: string]: Cb;
|
||||
};
|
||||
|
||||
class Callbacks {
|
||||
callbacks: Cbs = {};
|
||||
id = 0;
|
||||
|
||||
register(fn: () => void, once = true): () => void {
|
||||
this.id += 1;
|
||||
const id = this.id;
|
||||
|
||||
this.callbacks[id] = { fn, once };
|
||||
return () => {
|
||||
delete this.callbacks[id];
|
||||
};
|
||||
}
|
||||
|
||||
invoke(): void {
|
||||
for (const id in this.callbacks) {
|
||||
const cb = this.callbacks[id];
|
||||
|
||||
try {
|
||||
cb.fn();
|
||||
} finally {
|
||||
if (cb.once) delete this.callbacks[id];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.callbacks = {};
|
||||
}
|
||||
|
||||
count(): number {
|
||||
return Object.keys(this.callbacks).length;
|
||||
}
|
||||
}
|
||||
|
||||
export { Callbacks };
|
||||
3
srcts/types/src/bindings/registry.d.ts
vendored
3
srcts/types/src/bindings/registry.d.ts
vendored
@@ -1,4 +1,3 @@
|
||||
import { Callbacks } from "../utils/callbacks";
|
||||
interface BindingBase {
|
||||
name: string;
|
||||
}
|
||||
@@ -13,9 +12,7 @@ declare class BindingRegistry<Binding extends BindingBase> {
|
||||
bindingNames: {
|
||||
[key: string]: BindingObj<Binding>;
|
||||
};
|
||||
registerCallbacks: Callbacks;
|
||||
register(binding: Binding, bindingName: string, priority?: number): void;
|
||||
onRegister(fn: () => void, once?: boolean): void;
|
||||
setPriority(bindingName: string, priority: number): void;
|
||||
getPriority(bindingName: string): number | false;
|
||||
getBindings(): Array<BindingObj<Binding>>;
|
||||
|
||||
4
srcts/types/src/shiny/render.d.ts
vendored
4
srcts/types/src/shiny/render.d.ts
vendored
@@ -7,10 +7,6 @@ declare function renderContent(el: BindScope, content: string | {
|
||||
deps?: HtmlDep[];
|
||||
} | null, where?: WherePosition): void;
|
||||
declare function renderHtml(html: string, el: BindScope, dependencies: HtmlDep[], where?: WherePosition): ReturnType<typeof singletonsRenderHtml>;
|
||||
declare namespace renderHtml {
|
||||
var _renderCount: number;
|
||||
var isExecuting: () => boolean;
|
||||
}
|
||||
declare type HtmlDepVersion = string;
|
||||
declare type MetaItem = {
|
||||
name: string;
|
||||
|
||||
16
srcts/types/src/utils/callbacks.d.ts
vendored
16
srcts/types/src/utils/callbacks.d.ts
vendored
@@ -1,16 +0,0 @@
|
||||
declare type Cb = {
|
||||
once: boolean;
|
||||
fn: () => void;
|
||||
};
|
||||
declare type Cbs = {
|
||||
[key: string]: Cb;
|
||||
};
|
||||
declare class Callbacks {
|
||||
callbacks: Cbs;
|
||||
id: number;
|
||||
register(fn: () => void, once?: boolean): () => void;
|
||||
invoke(): void;
|
||||
clear(): void;
|
||||
count(): number;
|
||||
}
|
||||
export { Callbacks };
|
||||
@@ -45,12 +45,12 @@ test_that("Inputs and values in query string", {
|
||||
suppress_stacktrace(expect_warning(expect_warning(RestoreContext$new("?_inputs_&a=1&_inputs_&b=2"))))
|
||||
suppress_stacktrace(expect_warning(expect_warning(RestoreContext$new("?_inputs_&a=1&_values_&b=2&_inputs_&"))))
|
||||
suppress_stacktrace(expect_warning(expect_warning(RestoreContext$new("?_values_&a=1&_values_"))))
|
||||
suppress_stacktrace(expect_warning(RestoreContext$new("?_inputs_&a=1&_values_&_values&b=2")))
|
||||
suppress_stacktrace(expect_warning(expect_warning(RestoreContext$new("?_inputs_&a=1&_values_&_values&b=2"))))
|
||||
|
||||
# If there's an error in the conversion from query string, should have
|
||||
# blank values.
|
||||
suppress_stacktrace(expect_warning(rc <- RestoreContext$new("?_inputs_&a=[x&b=1")))
|
||||
expect_identical(rc$input$asList(), list(b=1L))
|
||||
suppress_stacktrace(expect_warning(expect_warning(rc <- RestoreContext$new("?_inputs_&a=[x&b=1"))))
|
||||
expect_identical(rc$input$asList(), list())
|
||||
expect_identical(as.list(rc$values), list())
|
||||
expect_identical(rc$dir, NULL)
|
||||
|
||||
|
||||
@@ -13,28 +13,3 @@ test_that("can access reactive values directly", {
|
||||
y <- reactive(x1() + x2$a)
|
||||
expect_equal(y(), 4)
|
||||
})
|
||||
|
||||
test_that("errors in throttled/debounced reactives are catchable", {
|
||||
reactiveConsole(TRUE)
|
||||
on.exit(reactiveConsole(FALSE))
|
||||
|
||||
# In Shiny 1.7 and earlier, if a throttled/debounced reactive threw an error,
|
||||
# it would cause internal observers used by the implementations of
|
||||
# debounce/throttle to error, which would kill the session. The correct
|
||||
# behavior is to only expose the error to consumers of the throttled/debounced
|
||||
# reactive.
|
||||
|
||||
r <- reactive({
|
||||
stop("boom")
|
||||
})
|
||||
|
||||
rd <- r %>% debounce(1000)
|
||||
rt <- r %>% throttle(1000)
|
||||
|
||||
observe({
|
||||
try(rd(), silent = TRUE)
|
||||
try(rt(), silent = TRUE)
|
||||
})
|
||||
|
||||
expect_silent(flushReact())
|
||||
})
|
||||
|
||||
@@ -1,33 +1,30 @@
|
||||
foo <- function() {
|
||||
capture <- function() {
|
||||
list(
|
||||
calls = sys.calls(),
|
||||
parents = sys.parents()
|
||||
)
|
||||
}
|
||||
|
||||
capture_1 <- function() {
|
||||
capture()
|
||||
}
|
||||
|
||||
capture_2 <- function() {
|
||||
capture_1()
|
||||
}
|
||||
|
||||
do.call(
|
||||
identity,
|
||||
list(
|
||||
identity(capture_2())
|
||||
)
|
||||
capture <- function() {
|
||||
list(
|
||||
calls = sys.calls(),
|
||||
parents = sys.parents()
|
||||
)
|
||||
}
|
||||
res <- foo()
|
||||
res$calls <- tail(res$calls, 6)
|
||||
res$parents <- tail(res$parents - (length(res$parents) - 6), 6)
|
||||
|
||||
capture_1 <- function() {
|
||||
capture()
|
||||
}
|
||||
|
||||
capture_2 <- function() {
|
||||
capture_1()
|
||||
}
|
||||
|
||||
res <- do.call(
|
||||
identity,
|
||||
list(
|
||||
identity(capture_2())
|
||||
)
|
||||
)
|
||||
res$calls <- tail(res$calls, 5)
|
||||
res$parents <- tail(res$parents - (length(res$parents) - 5), 5)
|
||||
|
||||
describe("stack pruning", {
|
||||
it("passes basic example", {
|
||||
expect_equal(pruneStackTrace(res$parents), c(T, F, F, T, T, T))
|
||||
expect_equal(lapply(list(res$parents), pruneStackTrace), list(c(T, F, F, T, T, T)))
|
||||
expect_equal(pruneStackTrace(res$parents), c(F, F, T, T, T))
|
||||
expect_equal(lapply(list(res$parents), pruneStackTrace), list(c(F, F, T, T, T)))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
{
|
||||
"declaration": true,
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"target": "ES5",
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"declaration": true,
|
||||
"declarationDir": "./srcts/types",
|
||||
"emitDeclarationOnly": true,
|
||||
"moduleResolution": "node",
|
||||
// Can not use `types: []` to disable injecting NodeJS types. More types are
|
||||
// needed than just the DOM's `window.setTimeout`
|
||||
// "types": [],
|
||||
|
||||
Reference in New Issue
Block a user