Compare commits

..

14 Commits

Author SHA1 Message Date
Carson
f694e708c1 Add comment; removing logging 2023-03-31 10:28:39 -05:00
Carson
66a972604e Use actionQueue to guarantee bind happens after async rendering has completed 2023-03-31 10:23:18 -05:00
Carson
91df0b15b7 Update news 2023-03-29 14:57:23 -05:00
Carson
a1e5514f81 Merge branch 'main' into bindAfterRegister 2023-03-29 14:49:40 -05:00
Carson
d93136ca1b lint; yarn build 2023-03-29 14:47:20 -05:00
Carson Sievert
4702ff480c Update srcts/src/shiny/init.ts 2023-03-29 14:41:59 -05:00
Carson Sievert
490803fc10 Update srcts/src/shiny/init.ts 2023-03-29 14:36:30 -05:00
Carson Sievert
1f752b6420 Merge branch 'main' into bindAfterRegister 2023-01-18 15:15:54 -06:00
wch
0f00ecfc20 Sync package version (GitHub Actions) 2023-01-06 22:08:33 +00:00
Winston Chang
0fe804012a Merge branch 'main' into bindAfterRegister 2023-01-06 16:00:57 -06:00
Winston Chang
ed12e92d60 Merge branch 'main' into bindAfterRegister 2022-10-05 12:46:13 -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
21 changed files with 1903 additions and 1704 deletions

View File

@@ -6,12 +6,14 @@
### New features and improvements
* Closed #789: Dynamic UI is now rendered asynchronously, thanks in part to the newly exported `Shiny.renderDependenciesAsync()`, `Shiny.renderHtmlAsync()`, and `Shiny.renderContentAsync()`. Importantly, this means `<script>` tags are now loaded asynchronously (the old way used `XMLHttpRequest`, which is synchronous). In addition, `Shiny` now manages a queue of async tasks (exposed via `Shiny.shinyapp.taskQueue`) so that order of execution is preserved. (#3666)
* Closed #789: `<script>` loaded from dynamic UI are no longer loaded using synchronous `XMLHttpRequest` (via jQuery). (#3666)
* For `reactiveValues()` objects, whenever the `$names()` or `$values()` methods are called, the keys are now returned in the order that they were inserted. (#3774)
* `Map` objects are now initialized at load time instead of build time. This avoids potential problems that could arise from storing `fastmap` objects into the built Shiny package. (#3775)
* Closed #3635: `window.Shiny.outputBindings` and `window.Shiny.inputBindings` gain a `onRegister()` method, to register callbacks that execute whenever a new binding is registered. Internally, Shiny uses this to check whether it should re-bind to the DOM when a binding has been registered. (#3638)
### Bug fixes
* Fixed #3771: Sometimes the error `ion.rangeSlider.min.js: i.stopPropagation is not a function` would appear in the JavaScript console. (#3772)

View File

@@ -7,17 +7,6 @@ NULL
.globals$appState <- NULL
#' Check whether a Shiny application is running
#'
#' This function tests whether a Shiny application is currently running.
#'
#' @return `TRUE` if a Shiny application is currently running. Otherwise,
#' `FALSE`.
#' @export
isRunning <- function() {
!is.null(getCurrentAppState())
}
initCurrentAppState <- function(appobj) {
if (!is.null(.globals$appState)) {
stop("Can't initialize current app state when another is currently active.")
@@ -32,14 +21,6 @@ getCurrentAppState <- function() {
.globals$appState
}
getCurrentAppStateOptions <- function() {
.globals$appState$options
}
setCurrentAppStateOptions <- function(options) {
stopifnot(isRunning())
.globals$appState$options <- options
}
clearCurrentAppState <- function() {
.globals$appState <- NULL
}

View File

@@ -1200,25 +1200,19 @@ uiOutput <- htmlOutput
#' @examples
#' \dontrun{
#' ui <- fluidPage(
#' p("Choose a dataset to download."),
#' selectInput("dataset", "Dataset", choices = c("mtcars", "airquality")),
#' downloadButton("downloadData", "Download")
#' )
#'
#' server <- function(input, output) {
#' # The requested dataset
#' data <- reactive({
#' get(input$dataset)
#' })
#' # Our dataset
#' data <- mtcars
#'
#' output$downloadData <- downloadHandler(
#' filename = function() {
#' # Use the selected dataset as the suggested file name
#' paste0(input$dataset, ".csv")
#' paste("data-", Sys.Date(), ".csv", sep="")
#' },
#' content = function(file) {
#' # Write the dataset to the `file` that will be downloaded
#' write.csv(data(), file)
#' write.csv(data, file)
#' }
#' )
#' }

View File

@@ -274,7 +274,7 @@ MockShinySession <- R6Class(
self$token <- createUniqueId(16)
# Copy app-level options
self$options <- getCurrentAppStateOptions()
self$options <- getCurrentAppState()$options
self$cache <- cachem::cache_mem()
self$appcache <- cachem::cache_mem()

View File

@@ -495,6 +495,16 @@ serviceApp <- function() {
.shinyServerMinVersion <- '0.3.4'
#' Check whether a Shiny application is running
#'
#' This function tests whether a Shiny application is currently running.
#'
#' @return `TRUE` if a Shiny application is currently running. Otherwise,
#' `FALSE`.
#' @export
isRunning <- function() {
!is.null(getCurrentAppState())
}
# Returns TRUE if we're running in Shiny Server or other hosting environment,

View File

@@ -19,10 +19,10 @@ getShinyOption <- function(name, default = NULL) {
}
# Check if there's a current app
if (isRunning()) {
app_state_options <- getCurrentAppStateOptions()
if (name %in% names(app_state_options)) {
return(app_state_options[[name]])
app_state <- getCurrentAppState()
if (!is.null(app_state)) {
if (name %in% names(app_state$options)) {
return(app_state$options[[name]])
} else {
return(default)
}
@@ -199,12 +199,11 @@ shinyOptions <- function(...) {
# If not in a session, but we have a currently running app, modify options
# at the app level.
if (isRunning()) {
app_state <- getCurrentAppState()
if (!is.null(app_state)) {
# Modify app-level options
setCurrentAppStateOptions(
dropNulls(mergeVectors(getCurrentAppStateOptions(), newOpts))
)
return(invisible(getCurrentAppStateOptions()))
app_state$options <- dropNulls(mergeVectors(app_state$options, newOpts))
return(invisible(app_state$options))
}
# If no currently running app, modify global options and return them.
@@ -219,8 +218,9 @@ shinyOptions <- function(...) {
return(session$options)
}
if (isRunning()) {
return(getCurrentAppStateOptions())
app_state <- getCurrentAppState()
if (!is.null(app_state)) {
return(app_state$options)
}
return(.globals$options)

View File

@@ -738,7 +738,7 @@ ShinySession <- R6Class(
private$.outputOptions <- list()
# Copy app-level options
self$options <- getCurrentAppStateOptions()
self$options <- getCurrentAppState()$options
self$cache <- cachem::cache_mem(max_size = 200 * 1024^2)

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

@@ -36,25 +36,19 @@ function.
\examples{
\dontrun{
ui <- fluidPage(
p("Choose a dataset to download."),
selectInput("dataset", "Dataset", choices = c("mtcars", "airquality")),
downloadButton("downloadData", "Download")
)
server <- function(input, output) {
# The requested dataset
data <- reactive({
get(input$dataset)
})
# Our dataset
data <- mtcars
output$downloadData <- downloadHandler(
filename = function() {
# Use the selected dataset as the suggested file name
paste0(input$dataset, ".csv")
paste("data-", Sys.Date(), ".csv", sep="")
},
content = function(file) {
# Write the dataset to the `file` that will be downloaded
write.csv(data(), file)
write.csv(data, file)
}
)
}

View File

@@ -1,5 +1,5 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/app-state.R
% Please edit documentation in R/server.R
\name{isRunning}
\alias{isRunning}
\title{Check whether a Shiny application is running}

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

@@ -22,7 +22,7 @@ class InputBatchSender implements InputPolicy {
if (opts.priority === "event") {
this._sendNow();
} else if (!this.sendIsEnqueued) {
this.shinyapp.taskQueue.enqueue(() => {
this.shinyapp.actionQueue.enqueue(() => {
this.sendIsEnqueued = false;
this._sendNow();
});

View File

@@ -150,6 +150,24 @@ function initShiny(windowShiny: Shiny): void {
(x) => x.value
);
// When future bindings are registered, rebind to the DOM once the current
// event loop is done. This is necessary since the binding might be registered
// after Shiny has already bound to the DOM (#3635)
let enqueuedCount = 0;
const enqueueRebind = () => {
enqueuedCount++;
windowShiny.shinyapp?.actionQueue.enqueue(() => {
enqueuedCount--;
// If this function is scheduled more than once in the queue, don't do anything.
// Only do the bindAll when we get to the last instance of this function in the queue.
if (enqueuedCount === 0) {
windowShiny.bindAll?.(document.documentElement);
}
});
};
inputBindings.onRegister(enqueueRebind, false);
outputBindings.onRegister(enqueueRebind, 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

@@ -114,7 +114,7 @@ class ShinyApp {
// without overlapping. This is used for handling incoming messages from the
// server and scheduling outgoing messages to the server, and can be used for
// other things tasks as well.
taskQueue = new AsyncQueue<() => Promise<void> | void>();
actionQueue = new AsyncQueue<() => Promise<void> | void>();
config: {
workerId: string;
@@ -240,7 +240,7 @@ class ShinyApp {
this.startActionQueueLoop();
};
socket.onmessage = (e) => {
this.taskQueue.enqueue(async () => await this.dispatchMessage(e.data));
this.actionQueue.enqueue(async () => await this.dispatchMessage(e.data));
};
// Called when a successfully-opened websocket is closed, or when an
// attempt to open a connection fails.
@@ -266,7 +266,7 @@ class ShinyApp {
async startActionQueueLoop(): Promise<void> {
// eslint-disable-next-line no-constant-condition
while (true) {
const action = await this.taskQueue.dequeue();
const action = await this.actionQueue.dequeue();
try {
await action();

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

@@ -20,7 +20,7 @@ type MessageValue = Parameters<WebSocket["send"]>[0];
declare function addCustomMessageHandler(type: string, handler: Handler): void;
declare class ShinyApp {
$socket: ShinyWebSocket | null;
taskQueue: AsyncQueue<() => Promise<void> | void>;
actionQueue: AsyncQueue<() => Promise<void> | void>;
config: {
workerId: string;
sessionId: string;

16
srcts/types/src/utils/callbacks.d.ts vendored Normal file
View File

@@ -0,0 +1,16 @@
type Cb = {
once: boolean;
fn: () => void;
};
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 };