闪亮的应用程序下载按钮和 future_promise:enc2utf8 错误:参数不是字符向量

shiny app download button and future_promise: Error in enc2utf8: argument is not a character vector

我正在尝试在 shiny 中创建一个下载处理程序,但使用 future_promise() 因为写入文件可能需要一些时间。这是我想做的工作示例,但不使用异步框架:

一个有效的 .Rmd 闪亮应用程序:当您单击该按钮时,它会将 10 个随机偏差写入一个文件并提供下载。我添加了 5 秒的延迟。

---
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")
})

我正在尝试使用 future_promise 在异步框架中实现它。这是{future}/{promises} 版本:

---
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")
})

当我在 Firefox 中单击按钮时,我没有得到任何文件,在 R 控制台中,显示如下:

Warning: Error in enc2utf8: argument is not a character vector
  [No stack trace available]

经过一些调试,我相信这是因为 运行 下载处理程序是 运行 filename 函数,需要一个字符向量,并得到一个承诺。但我不确定如何解决这个问题。

我看到这个question,提问者似乎有同样的问题,但没有提供解决方案(他们的例子不可重现)。

我该如何解决这个问题?

Promises 与 R Markdown 一起使用,但也有好消息和坏消息。

好消息

承诺在 downloadHandler 上工作

简而言之,可以使用 promises 代替 return 值:它只是在稍后某个时间点提供的输出值。因此,对于任何输出对象,包括 downloadHandler,您可以提供承诺而不是输出值。

一个 promise 由一个 future_promise() 函数组成,它执行一些缓慢的 运行 操作(通常在不同的 R 会话中)和一个解决部分(这是 [= 后面的部分) 15=] operator) 获取结果并提供解决方案。两者的组合就是 promise.

downloadHandler 有点特殊,因为它不接收对象作为输出,但希望将名称为 f 的文件写入磁盘(因此 NULL return 值)。您的原始代码 return 编辑了 close(con),这阻碍了代码的工作(但不是错误的原因)。

要在 downloadHandler 上使用 promise,必须将文件写入磁盘替换为 promise。但是,在您的代码中,您的最后一行是 close(con),这不是一个承诺。因此,首要任务是将文件写入函数卸载,然后它可以成为未来构造的解析部分。

downloadHandler 似乎不支持 filename 部分的承诺,如@Waldi 所述。我没有这方面的支持信息。

坏消息

Promise 在 R markdown 上下文中没有多大意义

in this article 所述,可以在 Shiny 上下文中使用 promises,并防止服务器 跨会话 锁定。在单个会话中,事件循环等待所有承诺在呈现输出之前解决,有效地导致我们都学会喜欢的完全相同的卡住 UI。只有当第二个会话处于活动状态时,承诺才会产生任何性能优势。

使用 promises 的 downloadHandler 的完整示例

下面的代码是你上面代码的改编,有三个小的区别:

  • 各种 future 和 resolve 函数已经隔离
  • downloadHandler filename 参数现在是静态的
  • downloadHandler content 参数提供了完整的承诺

保留序言

---
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)
```

为了更清晰,定义两个独立函数。请注意 writeFile 处理所有 I/O ,包括关闭连接

```{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 部分:注意内容现在 return 是一个承诺。

```{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")
})
```