1. Event-driven promises
mirai provides an as.promise() method for
conversion to promises
package promises. See the promises articles for a
comprehensive guide.
Use mirai directly with: - Promise pipe %...>%
(implicitly calls as.promise()) - Promise-aware functions
(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:
- Actions trigger immediately on resolution (no time-polling)
- Data already received in background (no transfer delay)
- High responsiveness (zero latency) and massive scalability (thousands/millions of concurrent promises)
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.
2. Shiny ExtendedTask: Introduction
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:
-
UI: Use
bslib::input_task_button()(disables during computation):
input_task_button("btn", "Plot uniform distribution")-
Server: Create ExtendedTask with
ExtendedTask$new(), passing...tomirai(), bind to button:
task <- ExtendedTask$new(
function(...) mirai({Sys.sleep(y); runif(x)}, ...)
) |> bind_task_button("btn")- Server: Observe button input, invoke ExtendedTask with named arguments:
observeEvent(input$btn, task$invoke(x = input$n, y = input$delay))- Server: Render output consuming ExtendedTask result:
output$plot <- renderPlot(hist(task$result()))3. Shiny ExtendedTask: Cancellation
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.
4. Shiny ExtendedTask: Generative Art
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:
-
UI: Use
bslib::input_task_button():
input_task_button(ns("resample"), "Resample")-
Server: Create ExtendedTask with named arguments
passed through
environment():
task <- ExtendedTask$new(
function(time, run) mirai(run(time), environment())
) |> bind_task_button("resample")- Server: Observe button, invoke ExtendedTask with arguments:
observeEvent(input$resample, task$invoke(calc_time, run_task))- Server: Render output consuming result:
output$plot <- renderPlot(plot_result(task$result()))5. Shiny ExtendedTask: mirai map
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))6. Shiny Async: Coin Flips
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.
7. Shiny Async: Progress Bar
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.
8. Plumber GET Endpoint
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)9. Plumber POST Endpoint
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)