Compare commits

..

18 Commits

Author SHA1 Message Date
Winston Chang
2e934797c9 Insert <head> content after script tags 2022-07-07 22:01:01 -05:00
wch
581ace76e4 yarn build (GitHub Actions) 2022-07-07 20:01:30 +00:00
Winston Chang
e43609b60a Load script tags the normal way instead of with jQuery's synchronouse XHR 2022-07-07 14:54:29 -05:00
Carson
d0bf86e5e2 Add a clear() method to Callbacks 2022-07-07 13:37:37 -05:00
Carson
b023350b90 Introduce an onRegister() method on BindingRegistry to help solve the problem with sharing state 2022-07-07 13:36:21 -05:00
Carson
7bccfeb774 Close #3635: attempt another bind when registering a binding outside a renderHtml() context 2022-07-07 13:35:07 -05:00
Winston Chang
54e5a6b43c Merge branch 'dvg-p4-fix-throttle' 2022-07-05 20:03:22 -05:00
Winston Chang
9653cc2893 Rebuild shiny.js 2022-07-05 20:01:22 -05:00
Winston Chang
47dc5b4116 Code and comment cleanup 2022-07-05 19:37:44 -05:00
dvg-p4
9db9ef527a Fixed check for isPending and rebuilt javascript 2022-07-04 10:21:22 -04:00
dvg-p4
9285a1f7fc Update srcts/src/time/throttle.ts
Based on suggestion

Co-authored-by: Winston Chang <winston@stdout.org>
2022-07-01 19:02:26 -04:00
dvg-p4
d22eb1524a Updated NEWS.md 2022-07-01 17:15:09 -04:00
dvg-p4
5e3971c776 Fixed major bug in throttle.ts 2022-07-01 16:58:41 -04:00
Joe Cheng
ff5ef52dd5 Fix #3250 (#3602)
* Fix #3250

pruneStackTrace was interacting badly with dplyr errors. I'm still
not sure what causes these new cases, but the new behavior seems to
be much better, with no downside that I can think of.

* Fix existing unit tests

* Update news

Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2022-06-27 12:05:28 -05:00
Joe Cheng
634b1c7c3c Don't kill the session when a debounced/throttled reactive expr errors (#3624)
* Don't kill the session when a debounced/throttled reactive expr errors

Fixes #3581

* Update NEWS with PR number

Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
2022-06-27 10:57:10 -05:00
Carson Sievert
d4527cdc28 Use ragg::agg_png over Cairo::CairoPNG if available (#3654)
* Close #3626: use ragg::agg_png over Cairo::CairoPNG if available

* Update documentation
2022-06-24 17:50:58 -05:00
Carson Sievert
474f14003b Follow up to #3385: warn instead of message; update unit tests to reflect some parameters can now succeed when others fail (#3652) 2022-06-14 10:34:20 -05:00
Carson Sievert
8a5da25545 Fix/update news (#3651) 2022-06-14 09:18:51 -05:00
27 changed files with 2028 additions and 968 deletions

10
NEWS.md
View File

@@ -5,6 +5,8 @@ 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)
@@ -21,6 +23,8 @@ 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)
@@ -39,7 +43,11 @@ 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)
* 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)
* 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)
shiny 1.7.1

View File

@@ -370,7 +370,7 @@ RestoreContext <- R6Class("RestoreContext",
safeFromJSON(value),
error = function(e) {
varsUnparsed <<- c(varsUnparsed, name)
message("Failed to parse URL parameter \"", name, "\"")
warning("Failed to parse URL parameter \"", name, "\"")
}
)
}

View File

@@ -421,8 +421,17 @@ 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) {
if ((!is_dupe[[i]] && parents[[i]] == current_node) ||
parents[[i]] == 0 ||
parents[[i]] == i) {
current_node <<- i
TRUE
} else {

View File

@@ -1,19 +1,14 @@
startPNG <- function(filename, width, height, res, ...) {
# 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
pngfun <- if ((getOption('shiny.useragg') %||% TRUE) && is_installed("ragg")) {
ragg::agg_png
} else if (capabilities("aqua")) {
# i.e., png(type = 'quartz')
pngfun <- grDevices::png
grDevices::png
} else if ((getOption('shiny.usecairo') %||% TRUE) && is_installed("Cairo")) {
pngfun <- Cairo::CairoPNG
Cairo::CairoPNG
} else {
# i.e., png(type = 'cairo')
pngfun <- grDevices::png
grDevices::png
}
args <- rlang::list2(filename=filename, width=width, height=height, res=res, ...)
@@ -57,33 +52,31 @@ startPNG <- function(filename, width, height, res, ...) {
grDevices::dev.cur()
}
#' Run a plotting function and save the output as a PNG
#' Capture a plot as a PNG file.
#'
#' 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)`.
#' 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.
#'
#' @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
#' [grDevices::png()]. Note that this affects the resolution of PNG rendering in
#' @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
#' R; it won't change the actual ppi of the browser.
#' @param ... Arguments to be passed through to [grDevices::png()].
#' These can be used to set the width, height, background color, etc.
#' @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.
#'
#' @export
plotPNG <- function(func, filename=tempfile(fileext='.png'),
width=400, height=400, res=72, ...) {

View File

@@ -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
r()
try(r(), silent = TRUE)
return()
}
# This ensures r() is still tracked after firstRun
r()
try(r(), silent = TRUE)
# 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()
er()
try(er(), silent = TRUE)
}, 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(r(), {
observeEvent(try(r(), silent = TRUE), {
if (v$pending) {
# In a blackout period and someone already scheduled; do nothing
} else if (blackoutMillisLeft() > 0) {

View File

@@ -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 [grDevices::png()]. Note that this affects the resolution of PNG
#' passed to [plotPNG()]. 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 [grDevices::png()].
#' @param ... Arguments to be passed through to [plotPNG()].
#' 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

View File

@@ -140,9 +140,10 @@ 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.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.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.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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -2,7 +2,7 @@
% Please edit documentation in R/imageutils.R
\name{plotPNG}
\alias{plotPNG}
\title{Run a plotting function and save the output as a PNG}
\title{Capture a plot as a PNG file.}
\usage{
plotPNG(
func,
@@ -23,27 +23,26 @@ extension \code{.png}.}
\item{height}{Height in pixels.}
\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
\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
R; it won't change the actual ppi of the browser.}
\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{...}{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.
}
\description{
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.
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.
}
\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)}.
}

View File

@@ -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[grDevices:png]{grDevices::png()}}.
\item{...}{Arguments to be passed through to \code{\link[=plotPNG]{plotPNG()}}.
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

View File

@@ -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[grDevices:png]{grDevices::png()}}. Note that this affects the resolution of PNG
passed to \code{\link[=plotPNG]{plotPNG()}}. 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[grDevices:png]{grDevices::png()}}.
\item{...}{Arguments to be passed through to \code{\link[=plotPNG]{plotPNG()}}.
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

View File

@@ -119,9 +119,10 @@ 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.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.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.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.}
}

View File

@@ -1,4 +1,5 @@
import { mergeSort } from "../utils";
import { Callbacks } from "../utils/callbacks";
interface BindingBase {
name: string;
@@ -14,6 +15,7 @@ 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 };
@@ -23,6 +25,12 @@ 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 {

View File

@@ -21,7 +21,7 @@ import {
import { bindAll, unbindAll, _bindAll } from "./bind";
import type { BindInputsCtx, BindScope } from "./bind";
import { setShinyObj } from "./initedMethods";
import { registerDependency } from "./render";
import { registerDependency, renderHtml } from "./render";
import { sendImageSizeFns } from "./sendImageSize";
import { ShinyApp } from "./shinyapp";
import { registerNames as singletonsRegisterNames } from "./singletons";
@@ -150,6 +150,19 @@ 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(

View File

@@ -72,10 +72,20 @@ function renderHtml(
dependencies: HtmlDep[],
where: WherePosition = "replace"
): ReturnType<typeof singletonsRenderHtml> {
renderDependencies(dependencies);
return singletonsRenderHtml(html, el, where);
renderHtml._renderCount++;
try {
renderDependencies(dependencies);
return singletonsRenderHtml(html, el, where);
} finally {
renderHtml._renderCount--;
}
}
renderHtml._renderCount = 0;
renderHtml.isExecuting = function () {
return renderHtml._renderCount > 0;
};
type HtmlDepVersion = string;
type MetaItem = {
@@ -199,6 +209,9 @@ function renderDependency(dep_: HtmlDep) {
$head.append(stylesheetLinks);
}
const scriptPromises: Array<Promise<any>> = [];
const scriptElements: HTMLScriptElement[] = [];
dep.script.forEach((x) => {
const script = document.createElement("script");
@@ -210,9 +223,23 @@ function renderDependency(dep_: HtmlDep) {
script.setAttribute(attr, val ? val : "");
});
$head.append(script);
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);
});
// 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")
@@ -221,12 +248,22 @@ function renderDependency(dep_: HtmlDep) {
$head.append(link);
});
if (dep.head) {
const $newHead = $("<head></head>");
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);
});
$newHead.html(dep.head);
$head.append($newHead.children());
}
return true;
}

View File

@@ -19,40 +19,67 @@ 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.timerId !== null;
return this.args !== 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 && this.args.length > 0) {
this.func.apply(this.target, this.args);
} else {
this.func.apply(this.target);
if (this.args === null) {
// Shouldn't get here, because $invoke should only be called right after
// setting this.args. But just in case.
return;
}
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);
}
}

View File

@@ -0,0 +1,45 @@
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 };

View File

@@ -1,3 +1,4 @@
import { Callbacks } from "../utils/callbacks";
interface BindingBase {
name: string;
}
@@ -12,7 +13,9 @@ 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>>;

View File

@@ -7,6 +7,10 @@ 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 Normal file
View File

@@ -0,0 +1,16 @@
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 };

View File

@@ -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(expect_warning(RestoreContext$new("?_inputs_&a=1&_values_&_values&b=2"))))
suppress_stacktrace(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(expect_warning(rc <- RestoreContext$new("?_inputs_&a=[x&b=1"))))
expect_identical(rc$input$asList(), list())
suppress_stacktrace(expect_warning(rc <- RestoreContext$new("?_inputs_&a=[x&b=1")))
expect_identical(rc$input$asList(), list(b=1L))
expect_identical(as.list(rc$values), list())
expect_identical(rc$dir, NULL)

View File

@@ -13,3 +13,28 @@ 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())
})

View File

@@ -1,30 +1,33 @@
capture <- function() {
list(
calls = sys.calls(),
parents = sys.parents()
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_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)
res <- foo()
res$calls <- tail(res$calls, 6)
res$parents <- tail(res$parents - (length(res$parents) - 6), 6)
describe("stack pruning", {
it("passes basic example", {
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)))
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)))
})
})

View File

@@ -1,12 +1,13 @@
{
"declaration": true,
"compilerOptions": {
"target": "ES5",
"target": "es2020",
"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": [],