I am trying to create a download handler in shiny, but using future_promise() because it is possible that writing the file could take some time. Here is a working example of what I'd like to do, but without using the async framework:
A working .Rmd shiny app: when you click on the button, it writes 10 random deviates to a file and offers it as a download. I added a delay of 5 seconds.
---
title: "download, no futures"
runtime: shiny
output: html_document
---
```{r setup, include=FALSE}
library(dplyr)
knitr::opts_chunk$set(echo = FALSE)
```
This version works.
```{r}
renderUI({
button_reactive <- reactive({
y = rnorm(10)
Sys.sleep(5)
tf = tempfile(fileext = ".txt")
cat(c(y,'\n'), sep='\n', file = tf)
d = readBin(con = tf, what = "raw", n = file.size(tf))
return(list(fn = basename(tf), d = d))
})
output$button <- downloadHandler(
filename = function() {
button_reactive() %>%
`[[`('fn')
},
content = function(f) {
d = button_reactive() %>%
`[[`('d')
con = file(description = f, open = "wb")
writeBin(object = d, con = con)
close(con)
}
)
shiny::downloadButton(outputId = "button", label="Download")
})
I'm trying to implement this in the async framework using future_promise. Here's the {future}/{promises} version:
---
title: "download futures"
runtime: shiny
output: html_document
---
```{r setup, include=FALSE}
library(future)
library(promises)
plan(multisession)
library(dplyr)
knitr::opts_chunk$set(echo = FALSE)
```
This version yields this error on download attempt, reported in the R console:
```
Warning: Error in enc2utf8: argument is not a character vector
[No stack trace available]
```
```{r}
renderUI({
button_reactive <- reactive({
future_promise({
y = rnorm(10)
Sys.sleep(5)
tf = tempfile(fileext = ".txt")
cat(c(y,'\n'), sep='\n', file = tf)
d = readBin(con = tf, what = "raw", n = file.size(tf))
return(list(fn = basename(tf), d = d))
}, seed = TRUE)
})
output$button <- downloadHandler(
filename = function() {
button_reactive() %...>%
`[[`('fn')
},
content = function(f) {
con = file(description = f, open = "wb")
d = button_reactive() %...>%
`[[`('d') %...>%
writeBin(object = ., con = con)
close(con)
}
)
shiny::downloadButton(outputId = "button", label="Download")
})
When I click the button in Firefox, I get no file and in the R console, this is shown:
Warning: Error in enc2utf8: argument is not a character vector
[No stack trace available]
After some debugging, I believe this occurs because whatever is running the download handler is running the filename
function, expecting a character vector, and getting a promise. But I'm not sure how to fix this.
I saw this question, in which the asker seems to have the same problem, but no solution was offered (and their example was not reproducible).
How can I fix this?
Promises work with R Markdown, but there is some good and bad news.
The good news
Promises work on downloadHandler
In brief, promises can be used in lieu of a return value: it is simply an output value that is provided at some later point in time. So for any output object, including the downloadHandler
, you can provide a promise rather than an output value.
A promise consists of a future_promise()
function, which performs some slow-running operation (typically in a different R session) and a resolution part (which is the part that follows the %...>%
operator) that picks up the results and provides resolution. The combination of both is the promise
.
The downloadHandler
is a bit special in that it doesn't receive an object as output, but expect a file of name f
written to disk (and therefore a NULL
return value). Your original code returned a close(con)
, which was a blocker to making the code work (but not the cause of the error).
For promises to work on downloadHandler
, the file-written-to-disk must be replaced by a promise. In your code, however, your last line was close(con)
, which is not a promise. First point of order is therefore to offload file Writing to a function, which can then be the resolving part of the future construct.
downloadHandler
doesn't seem to support promises for the filename
part, as mentioned by @Waldi. I have no supporting info for this.
The bad news
Promises don't make a lot of sense in an R markdown context
As explained in this article, promises can be used in a Shiny context, and prevent the locking up of the server across sessions. Within a single session, the event loop waits for all promises to resolve before rendering output, effective leading to the very same stuck UI that we've all learned to love. Only when a second session is active, will promises yield any performance benefit.
Full example for using downloadHandler with promises
The below code is an adaptation of your code above, with three small differences:
downloadHandler
filename
argument is now staticdownloadHandler
content
argument provides a full promiseKeeping the preamble
---
title: "download futures"
runtime: shiny
output: html_document
---
```{r setup, include=FALSE}
library(future)
library(promises)
plan(multisession)
library(dplyr)
knitr::opts_chunk$set(echo = FALSE)
```
Define two standalone function for more clarity. Note that writeFile
takes care of all I/O here, including closing the connection
```{r}
createFile = function(){
y = rnorm(10)
Sys.sleep(1)
tf = tempfile(fileext = ".txt")
cat(c(y,'\n'), sep='\n', file = tf)
d = readBin(con = tf, what = "raw", n = file.size(tf))
return(list(fn = basename(tf), d = d))
}
writeFile = function (fut, f){
x = fut[['d']]
con = file(description = f, open = "wb")
writeBin(object = x, con = con)
close(con)
}
```
UI part: note that content now returns a promise.
```{r}
renderUI({
testPromise = reactive({
future_promise({createFile()}, seed=T) %...>% (function (x) (x))()
})
fileName = reactive({
testPromise() %...>% '[['('fn')
})
output$button <- downloadHandler(
filename = function() {
'test.txt'
# This doesn't work - filename apparently doesn't support promises
# fileName()
},
content = function(f) {
# Content needs to receive promise as return value, so including resolution
testPromise() %...>% writeFile(., f)
}
)
shiny::downloadButton(outputId = "button", label="Download")
})
```
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With