| Title: | Reactive Notebooks for R |
|---|---|
| Description: | A reactive notebook engine for R in the style of Pluto.jl. Cells are read statically into a dependency graph, so editing a cell reruns the cells that depend on it. |
| Authors: | Jonathan Chen [aut, cre], The Pluto.jl authors [cph] (frontend in inst/frontend, forked from Pluto.jl (MIT)) |
| Maintainer: | Jonathan Chen <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.0.0.9000 |
| Built: | 2026-10-10 18:49:06 UTC |
| Source: | https://github.com/jowch/Ember |
edit_notebook() applies a batch of edit ops to a notebook at once.
Edits are staged: they change the code, the graph and the file, but never
run anything. set_code(), insert_cell(), delete_cell(),
move_cell(), fold_cell() and disable_cell() build the ops; they
are plain data until passed to edit_notebook().
edit_notebook(nb, ...) set_code(cell, code, expected = NULL) insert_cell(index, code = "", id = NULL) delete_cell(cell) move_cell(cell, index) fold_cell(cell, folded = TRUE) disable_cell(cell, disabled = TRUE)edit_notebook(nb, ...) set_code(cell, code, expected = NULL) insert_cell(index, code = "", id = NULL) delete_cell(cell) move_cell(cell, index) fold_cell(cell, folded = TRUE) disable_cell(cell, disabled = TRUE)
nb |
An |
... |
One or more ops made by |
cell |
A cell id or 1-based display index. |
code |
The cell's new code (a single string). |
expected |
The code the caller believes the cell currently has, as a
check before applying (Endeavor's optimistic-concurrency check). |
index |
1-based display position: where to insert ( |
id |
The new cell's id (a 36-character UUID), for a caller that names the
cell itself, as the browser does. |
folded |
|
disabled |
|
edit_notebook() returns, invisibly, a list with inserted (ids of
inserted cells, in op order) and seq (the state's seq after the batch).
If any op
in the batch cannot apply (an unknown id, a mismatched expected, an
attempt to disable a text cell, or code containing a line Ember reserves as a cell or footer
marker), the whole batch is refused and an ember_refused condition is
signalled; none of the ops take effect.
set_code(), insert_cell(), delete_cell(), move_cell(),
fold_cell() and disable_cell() each return an ember_op, a plain
list describing the edit; they do nothing on their own.
## Not run: nb <- open_notebook("growth.R") edit_notebook(nb, set_code(2, "deg <- 3")) edit_notebook(nb, insert_cell(3, "summary(fit)"), fold_cell(1)) edit_notebook(nb, disable_cell(2)) ## End(Not run)## Not run: nb <- open_notebook("growth.R") edit_notebook(nb, set_code(2, "deg <- 3")) edit_notebook(nb, insert_cell(3, "summary(fit)"), fold_cell(1)) edit_notebook(nb, disable_cell(2)) ## End(Not run)
Queries over an ember_graph (see notebook_graph): neighbours, run
order, per-cell summaries, which cells an edit or a worker report affects,
and what changed between two graph builds.
upstream(graph, id, transitive = FALSE) downstream(graph, id, transitive = FALSE) run_order(graph, ids = NULL) affected(old, new, changed) cell_errors(graph, id) blocked_cells(graph) cell_summary(graph, id) changed_cells(old, new) ## S3 method for class 'ember_graph' print(x, ...) ## S3 method for class 'ember_graph_error' format(x, ...)upstream(graph, id, transitive = FALSE) downstream(graph, id, transitive = FALSE) run_order(graph, ids = NULL) affected(old, new, changed) cell_errors(graph, id) blocked_cells(graph) cell_summary(graph, id) changed_cells(old, new) ## S3 method for class 'ember_graph' print(x, ...) ## S3 method for class 'ember_graph_error' format(x, ...)
graph |
An |
id |
A cell id. |
transitive |
|
ids |
Character vector of cell ids, or |
old, new
|
Two |
changed |
Character vector of ids that were just edited. |
x |
An |
... |
Unused; present for consistency with the generics. |
upstream(), downstream() return a character vector of cell ids.
run_order() returns graph$order filtered to ids (or all of it when
ids is NULL), in run order.
affected() returns, in run order, the cells to rerun or mark stale after
editing changed: changed itself, plus everything downstream of it in
new, plus anything downstream of it in old that still exists in new.
cell_errors() returns a list of ember_graph_error reported on id.
blocked_cells() returns a character vector of ids that can't run (named
in any graph error).
cell_summary() returns a list for one cell:
list(definitions, learned, references, packages, attaches, upstream,
downstream, errors, notes, formulas). definitions is static definitions
only (learned ones subtracted out, since learned reports them
separately).
changed_cells() returns a character vector of ids whose resolved view
(definitions, references, packages, upstream set, or errors) differs
between old and new, including ids added or deleted; present ids come
first in new's run order, followed by deleted ids in old's run order.
print.ember_graph() prints one line per cell (id, definitions, upstream
ids) followed by its errors, and returns x invisibly.
format.ember_graph_error() returns a one-line string, "[<kind>]
<message>".
g <- notebook_graph( cells = c(a = "x <- 1", b = "y <- x + 1") ) upstream(g, "b") downstream(g, "a", transitive = TRUE) run_order(g) cell_summary(g, "b")$references blocked_cells(g)g <- notebook_graph( cells = c(a = "x <- 1", b = "y <- x + 1") ) upstream(g, "b") downstream(g, "a", transitive = TRUE) run_order(g) cell_summary(g, "b")$references blocked_cells(g)
notebook_graph() builds the whole-notebook dependency graph from the
cells' code, the installed packages' exports, the disabled cells, and whatever
the worker has learned at run time. It is pure and deterministic: the same
inputs always give an identical graph. graph_learn() folds a new fact
reported by the worker (names it created, formula columns it read, or
global settings it changed) into
a graph, rebuilding only what that fact can change.
notebook_graph(cells, exports = list(), learned = NULL, disabled = character(), previous = NULL, read_file = NULL) graph_learn(graph, cell, definitions = NULL, references = NULL, settings = NULL)notebook_graph(cells, exports = list(), learned = NULL, disabled = character(), previous = NULL, read_file = NULL) graph_learn(graph, cell, definitions = NULL, references = NULL, settings = NULL)
cells |
Named character vector, cell id -> code, in display order. Markdown cells are passed as empty strings (or left out); they take part in the order and nothing else. |
exports |
Named list, package name -> character vector of exported
names (as from |
learned |
|
disabled |
Character ids of disabled cells (see |
previous |
An earlier |
read_file |
Reader for |
graph |
An |
cell |
The id of the cell the worker ran. |
definitions |
Character vector of names the cell defined at run time
(replaces, not adds to, any previously learned definitions for this cell),
or |
references |
Character vector of names the cell's formulas or dynamic
code read at run time, or |
settings |
Character vector of setting keys the cell changed at run
time (replaces any previously learned settings for this cell;
|
A settings cell (one that changes a global setting, statically or as
learned) runs first, after the cells it reads, and every later cell
depends on it through a "setting" edge. A setting or an attached
package in two cells is a graph error (setting_conflict,
package_conflict).
graph_learn() rebuilds with previous = graph, so only the affected
cell's analysis changes and the rest of the graph is reused. Learned
definitions take part in the multiple-definitions rule and in edges
exactly like statically read ones; learned references go through the same
edge resolution as static references.
Both return an ember_graph: a list with ids, settings (the settings cells, in display
order), analyses,
learned, exports, cells (per-cell resolved view), edges (a data
frame of from, to, name, via), upstream, downstream, order
(every id in run order), errors (a list of ember_graph_error), and
reread (ids whose analysis was actually recomputed in this build).
The session API: open or create a notebook file, stage runs, and read back
its state. A notebook handle (ember_notebook) is an opaque
environment owned by a background shell; callers never touch its fields
directly, they read it through notebook_snapshot(),
dependency_graph() and notebook_state().
open_notebook(path, repos = ember_repos(), cache = cache_dir()) new_notebook(path, repos = ember_repos(), cache = cache_dir()) close_notebook(nb) move_notebook(nb, path) set_cell_change_mode(nb, on_cell_change = c("autorun", "lazy")) allow_execution(nb) run_cells(nb, ids = NULL, wait = FALSE, timeout = 60) interrupt_notebook(nb) restart_notebook(nb) notebook_snapshot(nb) dependency_graph(nb) notebook_state(nb) render_png(nb, cell, width = NULL, height = NULL, res = 96, timeout = 5) on_notebook_event(nb, fn) wait_for(nb, predicate = is_idle, timeout = 10) ## S3 method for class 'ember_notebook' print(x, ...)open_notebook(path, repos = ember_repos(), cache = cache_dir()) new_notebook(path, repos = ember_repos(), cache = cache_dir()) close_notebook(nb) move_notebook(nb, path) set_cell_change_mode(nb, on_cell_change = c("autorun", "lazy")) allow_execution(nb) run_cells(nb, ids = NULL, wait = FALSE, timeout = 60) interrupt_notebook(nb) restart_notebook(nb) notebook_snapshot(nb) dependency_graph(nb) notebook_state(nb) render_png(nb, cell, width = NULL, height = NULL, res = 96, timeout = 5) on_notebook_event(nb, fn) wait_for(nb, predicate = is_idle, timeout = 10) ## S3 method for class 'ember_notebook' print(x, ...)
path |
Path to the notebook's |
repos |
|
cache |
|
nb |
An |
on_cell_change |
|
ids |
Cell ids or 1-based display indexes to run, or |
wait |
|
timeout |
Seconds to wait before giving up ( |
cell |
A cell id or 1-based display index. |
width, height
|
Requested pixel size for |
res |
Pixel density (dots per inch) for |
fn |
Function called as |
predicate |
Function of a snapshot (as returned by |
x |
An |
... |
Unused; present for consistency with |
Opening a notebook (open_notebook()) only parses the file and builds its
graph; no worker starts and nothing runs until run_cells() or
allow_execution() is called. run_cells() runs the requested cells and
any unrun, stale or failed ancestors first, in dependency order, one at a
time. A cell with a graph error of its own, or a text cell passed by id,
is returned in skipped rather than queued; a dependent of a failed or
graph-broken cell still runs, and fails on its own if it needs what that
cell would have provided.
The worker always starts from the notebook's own package library: its lock
is resolved and installed as needed (see packages), so there is no
separate library argument to hand it a different one.
on_notebook_event()'s note is list(kind, seq, ...) with kind one
of "notebook_opened", "cell_state", "topology_changed",
"file_saved", "execution_done", "notebook_shut_down".
open_notebook() and new_notebook() return an ember_notebook handle.
close_notebook() returns TRUE if the notebook was still in safe
preview (no worker running).
move_notebook() returns the new path. A running R process's working
directory follows the file, unless the notebook changed it.
allow_execution() returns TRUE if execution was already allowed.
run_cells() returns, invisibly, list(accepted, queued, skipped,
finished, timed_out).
notebook_snapshot() returns a plain list describing the notebook's
current, safe-to-keep state: list(id, path, seq, read_only, allowed,
process, restart_offered, worker_message, worker_memory, problems, order,
cells, packages), where process is one of "preview",
"starting", "ready", "busy", "stopped",
each cell's variables is a list of list(name, type) for the
globals it owns (no value or kind: a value the notebook
never printed stays out of this API), worker_memory is the worker's
last-reported memory in bytes (NULL until sampled for the current
worker), and packages is package_status(nb)'s value (see
packages).
dependency_graph() returns the notebook's current ember_graph (see
notebook_graph), reflecting staged edits.
notebook_state() returns the session's immutable internal state, for
advanced callers (the step-4 UI).
render_png() returns list(png = <raw> | NULL, mime = <mime type>).
on_notebook_event() returns a function that unsubscribes when called.
wait_for() returns TRUE if the predicate held, FALSE on timeout.
set_cell_change_mode(), interrupt_notebook() return NULL,
invisibly. restart_notebook() returns the dispatch reply, invisibly.
## Not run: nb <- open_notebook("growth.R") nb run_cells(nb, 4, wait = TRUE) snap <- notebook_snapshot(nb) snap$cells[[4]]$output$text interrupt_notebook(nb) restart_notebook(nb) close_notebook(nb) ## End(Not run)## Not run: nb <- open_notebook("growth.R") nb run_cells(nb, 4, wait = TRUE) snap <- notebook_snapshot(nb) snap$cells[[4]]$output$text interrupt_notebook(nb) restart_notebook(nb) close_notebook(nb) ## End(Not run)
A notebook detects the packages its code uses, resolves and locks their
versions at its snapshot date, and installs a library with renv. Nothing
about packages appears in the ordinary calls (edit_notebook(),
run_cells()): the lock fills in as soon as an edit is applied, and the
install starts on the first run. These functions are for reading that
status, changing the package set directly, moving the snapshot date, and
running or cleaning up outside a session.
package_status(nb) add_extra_package(name) remove_extra_package(name) preview_date(nb, date = Sys.Date(), wait = TRUE, apply = FALSE, timeout = 60) set_date(nb, date) run(path, repos = ember_repos(), cache = cache_dir(), echo = TRUE) clean(max_age = 60, cache = TRUE, dry_run = FALSE, dir = cache_dir(), now = Sys.time()) ember_repos(cran = "https://packagemanager.posit.co/cran", bioc = "https://packagemanager.posit.co/bioconductor", bioc_config = "https://bioconductor.org/config.yaml") cache_dir()package_status(nb) add_extra_package(name) remove_extra_package(name) preview_date(nb, date = Sys.Date(), wait = TRUE, apply = FALSE, timeout = 60) set_date(nb, date) run(path, repos = ember_repos(), cache = cache_dir(), echo = TRUE) clean(max_age = 60, cache = TRUE, dry_run = FALSE, dir = cache_dir(), now = Sys.time()) ember_repos(cran = "https://packagemanager.posit.co/cran", bioc = "https://packagemanager.posit.co/bioconductor", bioc_config = "https://bioconductor.org/config.yaml") cache_dir()
nb |
An |
name |
A package name, for |
date |
A date (anything |
wait |
|
apply |
|
timeout |
Seconds to wait before giving up. |
path |
Path to a notebook's |
repos |
An |
cache |
A |
echo |
|
max_age |
Days since last use after which an unused library or a
cached index is deleted by |
dry_run |
|
dir |
The cache directory |
now |
The time |
cran, bioc
|
Base repository URLs (no date), for |
bioc_config |
Where Bioconductor's list of releases comes from, for
|
set_date() changes the header's snapshot and the lock in one step and
saves the file; the library installs once execution is allowed, and if a
package the worker has already loaded changed version, the worker
restarts once the new library is ready, leaving every cell not run.
set_date() and an apply = TRUE preview that needs no restart both
apply the proposal the same way internally: neither is allowed to apply
anything but the current, ready proposal.
A page connected to the notebook sees an apply = TRUE preview as
ember$packages$update:
list(date, status, restart, changes, message), present only while such
a preview exists. A plain preview_date() call (no apply) is never
shown there: it is this function's own, R-side preview, not the Packages
tab's.
run() does not resolve: the file's lock is the authority. If the code
names a package the lock lacks, run() says so and runs anyway (R
reports the missing package), since resolving would change the file.
Building the library blocks the caller, the same installer and layout a
session uses.
clean() never deletes a library an open session in this process is using,
and never touches a user's own renv projects: Ember keeps its own renv
cache under cache_dir().
package_status() returns a plain list, the same value as
notebook_snapshot(nb)$packages: list(snapshot, r_version,
bioc_version, library = list(status, path, progress, message, log, failures),
packages = data.frame(name, version, source, direct, status, message),
problems, proposal, plan). Per package, status is one of
"installed", "installing", "missing", "failed" (named by
library$failures, or a direct package that needs one of them),
"not_installed" (every other package in a library that failed: its
staging folder is never renamed into place, so nothing in that install
actually landed), "not_found". plan is NULL or
list(install = <n packages>, restart = <names>), what running would do
(a "not_installed" package counts toward it exactly as "missing"
does).
library$log is the installer's last 200 lines from the most recent
failed install, whatever status is now: it isn't cleared by a later
successful install, so it can be stale (describing an earlier failure)
once status has moved past "failed". character() before any
install has ever failed. library$failures is a data frame
package, kind ("compile", "configure", "dependency",
"download", "other"), detail (the missing dependency's name for
"dependency", the first ERROR line for "other", NA otherwise),
needed_by (a list column: the direct packages whose dependency closure
reaches this one). Empty (0 rows) unless status is "failed".
add_extra_package()/remove_extra_package() return an ember_op, a
plain list, for use with edit_notebook(); they do nothing on their own.
remove_extra_package() is refused through edit_notebook() when the
name is also named directly in code (removing it from the header would do
nothing): edit the code instead.
preview_date() returns list(date, status, changes, problems, restart),
where status is "fetching", "ready" or "failed", and changes is
NULL or a data frame name, from, to, change ("added",
"removed", "upgraded", "downgraded", "source"). restart is
character() unless apply = TRUE left the proposal waiting rather than
applying it on its own: then it names the loaded packages applying would
restart.
set_date() returns, invisibly, the applied changes (the same shape
as preview_date()'s). It is refused (an ember_refused condition) unless
preview_date() for that date is ready and current: the caller must have
seen what would change.
run() returns, invisibly, the file's exit status.
clean() returns, invisibly, a data frame path, kind ("library",
"staging", "cache", "index"), bytes, last_used of what was (or
with dry_run, would be) deleted.
ember_repos() returns an ember_repos value. cache_dir() returns a
path: the ember.cache_dir option if set, otherwise
tools::R_user_dir("ember", "cache").
## Not run: nb <- open_notebook("growth.R") edit_notebook(nb, insert_cell(2, "library(dplyr)\n curves |> filter(od > 0.1)")) package_status(nb)$plan run_cells(nb, 2, wait = TRUE) package_status(nb)$library$status p <- preview_date(nb, Sys.Date()) p$changes set_date(nb, Sys.Date()) ember::run("growth.R") ember::clean(dry_run = TRUE) ## End(Not run)## Not run: nb <- open_notebook("growth.R") edit_notebook(nb, insert_cell(2, "library(dplyr)\n curves |> filter(od > 0.1)")) package_status(nb)$plan run_cells(nb, 2, wait = TRUE) package_status(nb)$library$status p <- preview_date(nb, Sys.Date()) p$changes set_date(nb, Sys.Date()) ember::run("growth.R") ember::clean(dry_run = TRUE) ## End(Not run)
Statically reads one cell's code: what it defines, what it reads, which
packages it attaches or uses, any global settings it changes, and the
formulas it builds. Pure: it never runs the code. This is the same reader
notebook_graph() uses for every cell.
read_cell(code, read_file = NULL)read_cell(code, read_file = NULL)
code |
A single string of R code. |
read_file |
Function of one path argument, used to follow a literal
|
An ember_cell_analysis: a list with code, parse_error (NULL
unless the code fails to parse), and data frames definitions,
references, packages, settings, sourced, notes, plus a list of
formula_site in formulas. When parse_error is set, every other
field is empty.
a <- read_cell("fit <- lm(y ~ poly(x, deg), data = df)") a$definitions$name a$references$name a$formulas[[1]]$columnsa <- read_cell("fit <- lm(y ~ poly(x, deg), data = df)") a$definitions$name a$references$name a$formulas[[1]]$columns
Host Pluto's frontend over HTTP and one websocket per browser tab, backed
by any number of ember_notebooks. start_server() is for the
interactive console: it runs the server in a background process and
returns at once. serve() is the blocking call behind it, for a host
(such as Endeavor) that starts its own R process and wants to open
notebooks itself.
serve(paths = character(), port = 0L, secret = random_secret(32), on_ready = NULL, launch_browser = FALSE, allowed_hosts = character()) start_server(path = NULL, port = 0L, open = interactive(), timeout = 15, allowed_hosts = character()) host_notebook(server, nb, owned = FALSE, remember = FALSE) stop_server(server) ## S3 method for class 'ember_server' print(x, ...) ## S3 method for class 'ember_server_handle' print(x, ...)serve(paths = character(), port = 0L, secret = random_secret(32), on_ready = NULL, launch_browser = FALSE, allowed_hosts = character()) start_server(path = NULL, port = 0L, open = interactive(), timeout = 15, allowed_hosts = character()) host_notebook(server, nb, owned = FALSE, remember = FALSE) stop_server(server) ## S3 method for class 'ember_server' print(x, ...) ## S3 method for class 'ember_server_handle' print(x, ...)
paths |
Notebook files to open in safe preview when |
port |
Port on |
secret |
The URL secret. Every page, static file request to
|
on_ready |
|
launch_browser |
Open the first notebook's edit URL (or the server's own URL if none was given) once the server is ready. |
allowed_hosts |
|
path |
A notebook file for |
open |
Open the notebook (or, with no |
timeout |
Seconds to wait for the child process to report that it is listening. |
server |
The |
nb |
An |
owned |
|
remember |
Internal: |
x |
An |
... |
Unused; present for consistency with |
serve() runs entirely on R's one thread: httpuv and the notebook
engine both use later, so nothing here blocks it. A browser tab's view
of a notebook is kept in sync by diffing the engine's current state against
what that tab was last sent, after every change and at the end of every
request; a page that changes nothing receives nothing.
A server's base URL (/) is its own start page: hosted and recently
opened notebooks, "New notebook" and "Open a file". New notebooks default
to the folder the server was started in. The recent list is a single file
per user under tools::R_user_dir("ember", "data"), shared by every
Ember server that user runs, whatever the port.
start_server() is for interactive use: running httpuv in the current
R session would freeze it whenever that session is busy running cells, so
the server runs in a child process instead, and the secret reaches it
through an environment variable rather than a command-line argument other
users on the machine could read.
serve() returns, invisibly, once stop_server() has been called from a
callback; it never returns on its own.
start_server() prints the link to open (the notebook's, given a
path; otherwise the start page's) as a message, and returns, invisibly,
an ember_server_handle: an environment with
url (the server's base URL, including ?secret=), open(path) (opens a
notebook through /open and returns its edit URL, invisibly),
stop() (kills the child process; its worker processes die with it),
output (the child process's recent output lines), and
process (the processx handle). While the child runs, a browser
that asks for a page without the current key gets the link printed again
in this console.
host_notebook() returns the notebook's edit URL (with the secret). If
the server already hosts that notebook, nothing changes and the same URL
is returned.
stop_server() returns NULL, invisibly.
## Not run: # At the console: the server runs in a child process. srv <- start_server("growth.R") srv srv$open("other.R") srv$stop() # Endeavor's host, blocking its own process. serve(port = 0L, secret = Sys.getenv("ENDEAVOR_EMBER_SECRET"), on_ready = function(server) { nb <- open_notebook("growth.R") url <- host_notebook(server, nb) message("edit at: ", url) }) ## End(Not run)## Not run: # At the console: the server runs in a child process. srv <- start_server("growth.R") srv srv$open("other.R") srv$stop() # Endeavor's host, blocking its own process. serve(port = 0L, secret = Sys.getenv("ENDEAVOR_EMBER_SECRET"), on_ready = function(server) { nb <- open_notebook("growth.R") url <- host_notebook(server, nb) message("edit at: ", url) }) ## End(Not run)