mirror of
https://github.com/rstudio/shiny.git
synced 2026-01-11 16:08:19 -05:00
Compare commits
14 Commits
set_app_st
...
bindAfterR
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f694e708c1 | ||
|
|
66a972604e | ||
|
|
91df0b15b7 | ||
|
|
a1e5514f81 | ||
|
|
d93136ca1b | ||
|
|
4702ff480c | ||
|
|
490803fc10 | ||
|
|
1f752b6420 | ||
|
|
0f00ecfc20 | ||
|
|
0fe804012a | ||
|
|
ed12e92d60 | ||
|
|
d0bf86e5e2 | ||
|
|
b023350b90 | ||
|
|
7bccfeb774 |
4
NEWS.md
4
NEWS.md
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
#' }
|
||||
#' )
|
||||
#' }
|
||||
|
||||
@@ -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()
|
||||
|
||||
10
R/server.R
10
R/server.R
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
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
@@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
45
srcts/src/utils/callbacks.ts
Normal file
45
srcts/src/utils/callbacks.ts
Normal 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 };
|
||||
3
srcts/types/src/bindings/registry.d.ts
vendored
3
srcts/types/src/bindings/registry.d.ts
vendored
@@ -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>>;
|
||||
|
||||
2
srcts/types/src/shiny/shinyapp.d.ts
vendored
2
srcts/types/src/shiny/shinyapp.d.ts
vendored
@@ -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
16
srcts/types/src/utils/callbacks.d.ts
vendored
Normal 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 };
|
||||
Reference in New Issue
Block a user