mirai provides an as.promise() method for conversion to promises package promises.
See the promises articles for a comprehensive guide.
Use mirai directly with:
%...>% (implicitly calls as.promise())promises::then(), shiny::ExtendedTask)Or explicitly convert with as.promise() to access $then(), $finally() methods.
Promises register actions triggered when mirai resolves.
This happens automatically when R is idle or within loops/functions calling later::run_now() (e.g., Shiny).
Mirai promises pass return values to onFulfilled (success) or errorValue to onRejected (error).
Event-driven advantages:
This outputs “hello” after one second:
library(mirai)
library(promises)
p <- mirai({Sys.sleep(1); "hello"}) %...>% cat()
p
#> <Promise [pending]>
Access mirai values at $data while using promises for side effects (assigning to an environment):
env <- new.env()
m <- mirai({
Sys.sleep(1)
"hello"
})
promises::then(m, function(x) env$res <- x)
m[]
#> [1] "hello"
After returning to the top level prompt:
env$res
#> [1] "hello"
mirai_map also has an as.promise() method.
It resolves when the entire map completes or any mirai is rejected.
mirai is the primary async backend for scaling Shiny applications.
Use daemons() to distribute tasks across local parallel processes or network resources.
Shiny ExtendedTask creates scalable apps responsive both intra-session (per user) and inter-session (multiple concurrent users).
In this example, the clock continues ticking while the expensive computation runs asynchronously. The button disables and plot greys out until completion.
Call
daemons()at top level. UseonStop()to automatically shut down daemons when the app exits.
library(shiny)
library(bslib)
library(mirai)
ui <- page_fluid(
p("The time is ", textOutput("current_time", inline = TRUE)),
hr(),
numericInput("n", "Sample size (n)", 100),
numericInput("delay", "Seconds to take for plot", 5),
input_task_button("btn", "Plot uniform distribution"),
plotOutput("plot")
)
server <- function(input, output, session) {
output$current_time <- renderText({
invalidateLater(1000)
format(Sys.time(), "%H:%M:%S %p")
})
task <- ExtendedTask$new(
function(...) mirai({Sys.sleep(y); runif(x)}, ...)
) |> bind_task_button("btn")
observeEvent(input$btn, task$invoke(x = input$n, y = input$delay))
output$plot <- renderPlot(hist(task$result()))
}
# run app using 1 local daemon
daemons(1)
# automatically shutdown daemons when app exits
onStop(function() daemons(0))
shinyApp(ui = ui, server = server)
Thanks to Joe Cheng for providing examples on which the above is based.
Key ExtendedTask components:
bslib::input_task_button() (disables during computation):input_task_button("btn", "Plot uniform distribution")
ExtendedTask$new(), passing ... to mirai(), bind to button:task <- ExtendedTask$new(
function(...) mirai({Sys.sleep(y); runif(x)}, ...)
) |> bind_task_button("btn")
observeEvent(input$btn, task$invoke(x = input$n, y = input$delay))
output$plot <- renderPlot(hist(task$result()))
This demonstrates cancellation, which works identically for local or remote tasks.
This adds an infinite sleep button that blocks execution (using one daemon). New tasks queue behind it. A cancel button stops the blocking task, resuming queued plots.
Assign a mirai reference in ExtendedTask$new(), then pass to stop_mirai():
library(shiny)
library(bslib)
library(mirai)
ui <- page_fluid(
p("The time is ", textOutput("current_time", inline = TRUE)),
hr(),
numericInput("n", "Sample size (n)", 100),
numericInput("delay", "Seconds to take for plot", 5),
input_task_button("btn", "Plot uniform distribution"),
hr(),
p("Click 'block' to suspend execution, and 'cancel' to resume"),
input_task_button("block", "Block"),
actionButton("cancel", "Cancel block"),
hr(),
plotOutput("plot")
)
server <- function(input, output, session) {
output$current_time <- renderText({
invalidateLater(1000)
format(Sys.time(), "%H:%M:%S %p")
})
task <- ExtendedTask$new(
function(...) mirai({Sys.sleep(y); runif(x)}, ...)
) |> bind_task_button("btn")
m <- NULL
block <- ExtendedTask$new(
function() m <<- mirai(Sys.sleep(Inf))
) |> bind_task_button("block")
observeEvent(input$btn, task$invoke(x = input$n, y = input$delay))
observeEvent(input$block, block$invoke())
observeEvent(input$cancel, stop_mirai(m))
observe({
updateActionButton(session, "cancel", disabled = block$status() != "running")
})
output$plot <- renderPlot(hist(task$result()))
}
# run app using 1 local daemon
daemons(1)
# automatically shutdown daemons when app exits
onStop(function() daemons(0))
shinyApp(ui = ui, server = server)
Thanks to Joe Cheng for providing examples on which the above is based.
This app generates spiral patterns asynchronously.
Users add multiple plots via Shiny modules, each with different calculation times.
Daemon limits become visible: with 3 daemons and 4 plots, the 4th waits for another to finish.
Wrapping runApp() in with(daemons(...), ...) sets up daemons for the app’s duration, exiting automatically on stop.
library(shiny)
library(mirai)
library(bslib)
library(ggplot2)
library(aRtsy)
# function definitions
run_task <- function(calc_time) {
Sys.sleep(calc_time)
list(
colors = aRtsy::colorPalette(name = "random", n = 3),
angle = runif(n = 1, min = - 2 * pi, max = 2 * pi),
size = 1,
p = 1
)
}
plot_result <- function(result) {
do.call(what = canvas_phyllotaxis, args = result)
}
# modules for individual plots
plotUI <- function(id, calc_time) {
ns <- NS(id)
card(
strong(paste0("Plot (calc time = ", calc_time, " secs)")),
input_task_button(ns("resample"), "Resample"),
plotOutput(ns("plot"), height="400px", width="400px")
)
}
plotServer <- function(id, calc_time) {
force(id)
force(calc_time)
moduleServer(
id,
function(input, output, session) {
task <- ExtendedTask$new(
function(time, run) mirai(run(time), environment())
) |> bind_task_button("resample")
observeEvent(input$resample, task$invoke(calc_time, run_task))
output$plot <- renderPlot(plot_result(task$result()))
}
)
}
# ui and server
ui <- page_sidebar(fillable = FALSE,
sidebar = sidebar(
numericInput("calc_time", "Calculation time (secs)", 5),
actionButton("add", "Add", class="btn-primary"),
),
layout_column_wrap(id = "results", width = "400px", fillable = FALSE)
)
server <- function(input, output, session) {
observeEvent(input$add, {
id <- nanonext::random(4)
insertUI("#results", where = "beforeEnd", ui = plotUI(id, input$calc_time))
plotServer(id, input$calc_time)
})
}
app <- shinyApp(ui, server)
# run app using 3 local daemons
with(daemons(3), runApp(app))
The above example builds on original code by Joe Cheng, Daniel Woodie and William Landau.
This uses environment() instead of ... to pass calling environment variables to mirai.
Key components:
bslib::input_task_button():input_task_button(ns("resample"), "Resample")
environment():task <- ExtendedTask$new(
function(time, run) mirai(run(time), environment())
) |> bind_task_button("resample")
observeEvent(input$resample, task$invoke(calc_time, run_task))
output$plot <- renderPlot(plot_result(task$result()))
mirai_map has an as.promise() method for direct use in ExtendedTask.
Resolves when the entire map completes or any mirai is rejected.
This performs multiple simultaneous calculations across daemons, returning results asynchronously:
library(shiny)
library(bslib)
library(mirai)
ui <- page_fluid(
titlePanel("ExtendedTask Map Demo"),
hr(),
p("The time is ", textOutput("current_time", inline = TRUE)),
p("Perform 4 calculations that each take between 1 and 4 secs to complete:"),
input_task_button("calculate", "Calculate"),
p(textOutput("result")),
tags$style(type="text/css", "#result {white-space: pre-wrap;}")
)
server <- function(input, output) {
task <- ExtendedTask$new(function() {
mirai_map(1:4, function(i) {
# simulated long calculation
Sys.sleep(i)
sprintf(
"Calc %d | PID %d | Finished at %s.", i, Sys.getpid(), format(Sys.time())
)
})
}) |> bind_task_button("calculate")
observeEvent(input$calculate, {
task$invoke()
})
output$result <- renderText({
# result of mirai_map() is a list
as.character(task$result())
}, sep = "\n")
output$current_time <- renderText({
invalidateLater(1000)
format(Sys.time(), "%H:%M:%S %p")
})
}
app <- shinyApp(ui, server)
with(daemons(4), runApp(app))
This integrates mirai_map() into a Shiny observer without ExtendedTask.
The ‘.promise’ argument registers promise actions for each mapped operation, updating reactive values or interacting with the app:
library(shiny)
library(mirai)
flip_coin <- function(...) {
Sys.sleep(0.1)
rbinom(n = 1, size = 1, prob = 0.501)
}
ui <- fluidPage(
div("Is the coin fair?"),
actionButton("task", "Flip 1000 coins"),
textOutput("status"),
textOutput("outcomes")
)
server <- function(input, output, session) {
# Keep running totals of heads, tails, and task errors
flips <- reactiveValues(heads = 0, tails = 0, flips = 0)
# Button to submit a batch of coin flips
observeEvent(input$task, {
mirai_map(
1:1000,
flip_coin,
.promise = \(x) {
if (x) flips$heads <- flips$heads + 1 else flips$tails <- flips$tails + 1
}
)
# Ensure there is something after mirai_map() in the observer, as it is
# convertible to a promise, and will otherwise be waited for before returning
flips$flips <- flips$flips + 1000
})
# Print time and task status
output$status <- renderText({
invalidateLater(millis = 1000)
time <- format(Sys.time(), "%H:%M:%S")
sprintf("%s | %s flips submitted", time, flips$flips)
})
# Print number of heads and tails
output$outcomes <- renderText(
sprintf("%s heads %s tails", flips$heads, flips$tails)
)
}
app <- shinyApp(ui = ui, server = server)
# run app using 8 local non-dispatcher daemons (tasks are the same length)
with(daemons(8, dispatcher = FALSE), {
# pre-load flip_coin function on all daemons for efficiency
everywhere({}, flip_coin = flip_coin)
runApp(app)
})
This is an adaptation of an original example provided by Will Landau for use of crew with Shiny. Please see https://wlandau.github.io/crew/articles/shiny.html.
This uses mirai_map() to update a Shiny progress bar with custom messages and a reactive value upon completion (asynchronously):
library(shiny)
library(mirai)
library(promises)
slow_squared <- function(x) {
Sys.sleep(runif(1))
x^2
}
ui <- fluidPage(
titlePanel("Asynchronous Squares Calculator"),
p("The time is ", textOutput("current_time", inline = TRUE)),
hr(),
actionButton("start", "Start Calculation"),
br(), br(),
uiOutput("progress_ui"),
verbatimTextOutput("result")
)
server <- function(input, output, session) {
x <- 1:100
y <- reactiveVal()
observeEvent(input$start, {
progress <- Progress$new(session, min = 0, max = length(x))
progress$set(message = "Parallel calculation in progress", detail = "Starting...")
completed <- reactiveVal(0)
mirai_map(
x,
slow_squared,
slow_squared = slow_squared,
.promise = function(result) {
new_val <- completed() + 1
completed(new_val) # Increment completed counter
progress$inc(1, detail = paste("Completed", new_val)) # Update progress
}
) %...>% {
y(unlist(.))
progress$close()
}
# Ensure there is something after mirai_map() in the observer, as otherwise
# the created promise will be waited for before returning
y(0)
})
output$current_time <- renderText({
invalidateLater(1000)
format(Sys.time(), "%H:%M:%S %p")
})
output$result <- renderPrint({
cat("Sum of squares calculated: ", sum(y()), "\n")
})
}
app <- shinyApp(ui, server)
with(daemons(8), runApp(app))
This example adapts a contribution from Davide Magno.
mirai serves as an async backend for plumber pipelines.
This runs the plumber router in a daemon process to avoid blocking (useful in interactive sessions; otherwise use code within the outer mirai() call directly).
The /echo endpoint accepts GET requests, sleeps 1 second (simulating expensive computation), and returns the ‘msg’ header with timestamp and process ID:
library(mirai)
daemons(1L, dispatcher = FALSE)
m <- mirai({
library(plumber)
library(promises) # to provide the promise pipe
library(mirai)
# more efficient not to use dispatcher if all requests are similar length
daemons(4L, dispatcher = FALSE) # handles 4 requests simultaneously
pr() |>
pr_get(
"/echo",
function(req, res) {
mirai(
{
Sys.sleep(1L)
list(
status = 200L,
body = list(
time = format(Sys.time()), msg = msg, pid = Sys.getpid()
)
)
},
msg = req$HEADERS$msg
) %...>% (function(x) {
res$status <- x$status
res$body <- x$body
})
}
) |>
pr_run(host = "127.0.0.1", port = 8985)
})
Query the API using an async HTTP client like nanonext::ncurl_aio().
All 8 requests submit at once, but responses have differing timestamps (only 4 process simultaneously due to daemon limit):
library(nanonext)
res <- lapply(
1:8,
function(i) ncurl_aio(
"http://127.0.0.1:8985/echo",
headers = c(msg = as.character(i))
)
)
collect_aio(res)
#> [[1]]
#> [1] "{\"error\":\"500 - Internal server error\"}"
#>
#> [[2]]
#> [1] "{\"error\":\"500 - Internal server error\"}"
#>
#> [[3]]
#> [1] "{\"error\":\"500 - Internal server error\"}"
#>
#> [[4]]
#> [1] "{\"error\":\"500 - Internal server error\"}"
#>
#> [[5]]
#> [1] "{\"error\":\"500 - Internal server error\"}"
#>
#> [[6]]
#> [1] "{\"error\":\"500 - Internal server error\"}"
#>
#> [[7]]
#> [1] "{\"error\":\"500 - Internal server error\"}"
#>
#> [[8]]
#> [1] "{\"error\":\"500 - Internal server error\"}"
daemons(0)
This uses a POST endpoint accepting JSON request data.
Always access req$postBody in the router process and pass to mirai as an argument (it uses a non-serializable connection):
library(mirai)
daemons(1L, dispatcher = FALSE)
m <- mirai({
library(plumber)
library(promises) # to provide the promise pipe
library(mirai)
# uses dispatcher - suitable when requests take differing times to complete
daemons(4L) # handles 4 requests simultaneously
pr() |>
pr_post(
"/echo",
function(req, res) {
mirai(
{
Sys.sleep(1L) # simulate expensive computation
list(
status = 200L,
body = list(
time = format(Sys.time()),
msg = jsonlite::parse_json(data)$msg,
pid = Sys.getpid()
)
)
},
data = req$postBody
) %...>% (function(x) {
res$status <- x$status
res$body <- x$body
})
}
) |>
pr_run(host = "127.0.0.1", port = 8986)
})
Querying produces the same output as the previous example:
library(nanonext)
res <- lapply(
1:8,
function(i) ncurl_aio(
"http://127.0.0.1:8986/echo",
method = "POST",
data = sprintf('{"msg":"%d"}', i)
)
)
collect_aio(res)
#> [[1]]
#> [1] "{\"time\":[\"2025-11-26 00:03:11\"],\"msg\":[\"1\"],\"pid\":[71207]}"
#>
#> [[2]]
#> [1] "{\"time\":[\"2025-11-26 00:03:12\"],\"msg\":[\"2\"],\"pid\":[71207]}"
#>
#> [[3]]
#> [1] "{\"time\":[\"2025-11-26 00:03:11\"],\"msg\":[\"3\"],\"pid\":[71217]}"
#>
#> [[4]]
#> [1] "{\"time\":[\"2025-11-26 00:03:11\"],\"msg\":[\"4\"],\"pid\":[71205]}"
#>
#> [[5]]
#> [1] "{\"time\":[\"2025-11-26 00:03:12\"],\"msg\":[\"5\"],\"pid\":[71217]}"
#>
#> [[6]]
#> [1] "{\"time\":[\"2025-11-26 00:03:12\"],\"msg\":[\"6\"],\"pid\":[71223]}"
#>
#> [[7]]
#> [1] "{\"time\":[\"2025-11-26 00:03:12\"],\"msg\":[\"7\"],\"pid\":[71205]}"
#>
#> [[8]]
#> [1] "{\"time\":[\"2025-11-26 00:03:11\"],\"msg\":[\"8\"],\"pid\":[71223]}"
daemons(0)