Compare commits

...

6 Commits

Author SHA1 Message Date
Barret Schloerke
f8a91fb2e3 Always disable UI cache (unless there's an existing Cache-Control header) 2023-04-18 21:34:18 -04:00
Barret Schloerke
7909389f0a Only disable cache if a theme has been set 2023-04-18 15:34:15 -04:00
Garrick Aden-Buie
596165e473 Set cache control headers to avoid caching UI 2023-04-18 12:22:31 -04:00
Winston Chang
62bb21d5b6 Merge pull request #3804 from rstudio/docs/ex-download-button 2023-04-14 15:48:19 -05:00
Garrick Aden-Buie
4f85268d44 More complete downloadButton() example 2023-04-13 09:04:40 -04:00
Carson Sievert
611e517bb8 Rename actionQueue to taskQueue, add more context to the NEWS item (#3801) 2023-03-31 14:38:28 -05:00
11 changed files with 60 additions and 27 deletions

View File

@@ -6,7 +6,7 @@
### New features and improvements
* Closed #789: `<script>` loaded from dynamic UI are no longer loaded using synchronous `XMLHttpRequest` (via jQuery). (#3666)
* 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)
* For `reactiveValues()` objects, whenever the `$names()` or `$values()` methods are called, the keys are now returned in the order that they were inserted. (#3774)

View File

@@ -1200,19 +1200,25 @@ 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) {
#' # Our dataset
#' data <- mtcars
#' # The requested dataset
#' data <- reactive({
#' get(input$dataset)
#' })
#'
#' output$downloadData <- downloadHandler(
#' filename = function() {
#' paste("data-", Sys.Date(), ".csv", sep="")
#' # Use the selected dataset as the suggested file name
#' paste0(input$dataset, ".csv")
#' },
#' content = function(file) {
#' write.csv(data, file)
#' # Write the dataset to the `file` that will be downloaded
#' write.csv(data(), file)
#' }
#' )
#' }

View File

@@ -231,11 +231,32 @@ uiHttpHandler <- function(ui, uiPattern = "^/$") {
if (is.null(uiValue))
return(NULL)
if (inherits(uiValue, "httpResponse")) {
return(uiValue)
} else {
# Avoid caching the UI response to ensure that UI is always re-evaluated.
# This is necessary for getCurrentTheme() to be known, required for BS4+.
no_cache_headers <- list(
"Cache-Control" = "no-cache, no-store, must-revalidate",
"Pragma" = "no-cache",
"Expires" = "0"
)
if (!inherits(uiValue, "httpResponse")) {
html <- renderPage(uiValue, showcaseMode, testMode)
return(httpResponse(200, content=html))
uiValue <- httpResponse(200, content=html)
}
# 2023-04-23 jcheng5: Example app in PR comment
# https://github.com/rstudio/shiny/pull/3810#issuecomment-1513828996
# > I think we should always add the cache-busting headers. Without it,
# Connect and ShinyApps.io configurations will have problems in that the
# workerId could be a stale value.
if (
# The user has not set a `Cache-Control` policy within their UI func
!"Cache-Control" %in% names(uiValue$headers)
) {
# Disable caching for the UI's response
uiValue$headers <- utils::modifyList(uiValue$headers, no_cache_headers)
}
uiValue
}
}

View File

@@ -15583,7 +15583,7 @@
if (opts.priority === "event") {
this._sendNow();
} else if (!this.sendIsEnqueued) {
this.shinyapp.actionQueue.enqueue(function() {
this.shinyapp.taskQueue.enqueue(function() {
_this.sendIsEnqueued = false;
_this._sendNow();
});
@@ -17666,7 +17666,7 @@
function ShinyApp2() {
_classCallCheck36(this, ShinyApp2);
_defineProperty18(this, "$socket", null);
_defineProperty18(this, "actionQueue", new AsyncQueue());
_defineProperty18(this, "taskQueue", new AsyncQueue());
_defineProperty18(this, "config", null);
_defineProperty18(this, "$inputValues", {});
_defineProperty18(this, "$initialInput", null);
@@ -17875,7 +17875,7 @@
_this.startActionQueueLoop();
};
socket.onmessage = function(e) {
_this.actionQueue.enqueue(/* @__PURE__ */ _asyncToGenerator8(/* @__PURE__ */ _regeneratorRuntime8().mark(function _callee2() {
_this.taskQueue.enqueue(/* @__PURE__ */ _asyncToGenerator8(/* @__PURE__ */ _regeneratorRuntime8().mark(function _callee2() {
return _regeneratorRuntime8().wrap(function _callee2$(_context2) {
while (1)
switch (_context2.prev = _context2.next) {
@@ -17918,7 +17918,7 @@
break;
}
_context3.next = 3;
return this.actionQueue.dequeue();
return this.taskQueue.dequeue();
case 3:
action = _context3.sent;
_context3.prev = 4;

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,19 +36,25 @@ 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) {
# Our dataset
data <- mtcars
# The requested dataset
data <- reactive({
get(input$dataset)
})
output$downloadData <- downloadHandler(
filename = function() {
paste("data-", Sys.Date(), ".csv", sep="")
# Use the selected dataset as the suggested file name
paste0(input$dataset, ".csv")
},
content = function(file) {
write.csv(data, file)
# Write the dataset to the `file` that will be downloaded
write.csv(data(), file)
}
)
}

View File

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

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.
actionQueue = new AsyncQueue<() => Promise<void> | void>();
taskQueue = new AsyncQueue<() => Promise<void> | void>();
config: {
workerId: string;
@@ -240,7 +240,7 @@ class ShinyApp {
this.startActionQueueLoop();
};
socket.onmessage = (e) => {
this.actionQueue.enqueue(async () => await this.dispatchMessage(e.data));
this.taskQueue.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.actionQueue.dequeue();
const action = await this.taskQueue.dequeue();
try {
await action();

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;
actionQueue: AsyncQueue<() => Promise<void> | void>;
taskQueue: AsyncQueue<() => Promise<void> | void>;
config: {
workerId: string;
sessionId: string;