In normal Shiny reactive code, whenever an observer, calc, or output is busy computing, it blocks the current session from receiving any inputs or attempting to proceed with any other computation related to that session.

The ExtendedTask class allows you to have an expensive operation that is started by a reactive effect, and whose (eventual) results can be accessed by a regular observer, calc, or output; but during the course of the operation, the current session is completely unblocked, allowing the user to continue using the rest of the app while the operation proceeds in the background.

Note that each ExtendedTask object does not represent a single invocation of its long-running function. Rather, it's an object that is used to invoke the function with different arguments, keeps track of whether an invocation is in progress, and provides ways to get at the current status or results of the operation. A single ExtendedTask object does not permit overlapping invocations: if the invoke() method is called before the previous invoke() is completed, the new invocation will not begin until the previous invocation has completed.

ExtendedTask versus asynchronous reactives

Shiny has long supported using {promises} to write asynchronous observers, calcs, or outputs. You may be wondering what the differences are between those techniques and this class.

Asynchronous observers, calcs, and outputs are not–and have never been–designed to let a user start a long-running operation, while keeping that very same (browser) session responsive to other interactions. Instead, they unblock other sessions, so you can take a long-running operation that would normally bring the entire R process to a halt and limit the blocking to just the session that started the operation. (For more details, see the section on "The Flush Cycle".)

ExtendedTask, on the other hand, invokes an asynchronous function (that is, a function that quickly returns a promise) and allows even that very session to immediately unblock and carry on with other user interactions.

Methods


Method new()

Creates a new ExtendedTask object. ExtendedTask should generally be created either at the top of a server function, or at the top of a module server function.

Usage

ExtendedTask$new(func)

Arguments

func

The long-running operation to execute. This should be an asynchronous function, meaning, it should use the {promises} package, most likely in conjuction with the {future} package. (In short, the return value of func should be a Future object, or a promise, or something else that promises::as.promise() understands.)

It's also important that this logic does not read from any reactive inputs/sources, as inputs may change after the function is invoked; instead, if the function needs to access reactive inputs, it should take parameters and the caller of the invoke() method should read reactive inputs and pass them as arguments.


Method invoke()

Starts executing the long-running operation. If this ExtendedTask is already running (meaning, a previous call to invoke() is not yet complete) then enqueues this invocation until after the current invocation, and any already-enqueued invocation, completes.

Usage

ExtendedTask$invoke(...)

Arguments

...

Parameters to use for this invocation of the underlying function. If reactive inputs are needed by the underlying function, they should be read by the caller of invoke and passed in as arguments.


Method status()

This is a reactive read that invalidates the caller when the task's status changes.

Returns one of the following values:

  • "initial": This ExtendedTask has not yet been invoked

  • "running": An invocation is currently running

  • "success": An invocation completed successfully, and a value can be retrieved via the result() method

  • "error": An invocation completed with an error, which will be re-thrown if you call the result() method

Usage

ExtendedTask$status()


Method result()

Attempts to read the results of the most recent invocation. This is a reactive read that invalidates as the task's status changes.

The actual behavior differs greatly depending on the current status of the task:

  • "initial": Throws a silent error (like req(FALSE)). If this happens during output rendering, the output will be blanked out.

  • "running": Throws a special silent error that, if it happens during output rendering, makes the output appear "in progress" until further notice.

  • "success": Returns the return value of the most recent invocation.

  • "error": Throws whatever error was thrown by the most recent invocation.

This method is intended to be called fairly naively by any output or reactive expression that cares about the output–you just have to be aware that if the result isn't ready for whatever reason, processing will stop in much the same way as req(FALSE) does, but when the result is ready you'll get invalidated, and when you run again the result should be there.

Note that the result() method is generally not meant to be used with observeEvent(), eventReactive(), bindEvent(), or isolate() as the invalidation will be ignored.

Usage

ExtendedTask$result()

Examples

if (FALSE) { # rlang::is_interactive() && rlang::is_installed("future")

library(shiny)
library(bslib)
library(future)
plan(multisession)

ui <- page_fluid(
  titlePanel("Extended Task Demo"),
  p(
    'Click the button below to perform a "calculation"',
    "that takes a while to perform."
  ),
  input_task_button("recalculate", "Recalculate"),
  p(textOutput("result"))
)

server <- function(input, output) {
  rand_task <- ExtendedTask$new(function() {
    future(
      {
        # Slow operation goes here
        Sys.sleep(2)
        sample(1:100, 1)
      },
      seed = TRUE
    )
  })

  # Make button state reflect task.
  # If using R >=4.1, you can do this instead:
  # rand_task <- ExtendedTask$new(...) |> bind_task_button("recalculate")
  bind_task_button(rand_task, "recalculate")

  observeEvent(input$recalculate, {
    # Invoke the extended in an observer
    rand_task$invoke()
  })

  output$result <- renderText({
    # React to updated results when the task completes
    number <- rand_task$result()
    paste0("Your number is ", number, ".")
  })
}

shinyApp(ui, server)
}