observablehq / runtime

The reactive dataflow runtime that powers Observable Framework and Observable notebooks
https://observablehq.com/@observablehq/how-observable-runs
ISC License
1k stars 71 forks source link

@observablehq/runtime

Node CI Greenkeeper badge

The Observable runtime lets you run Observable notebooks as true reactive programs in any JavaScript environment: on your personal website, integrated into your web application or interactive dashboard. Take your notebook to any distant shore the web platform reaches.

For example, to render the “hello” cell from the “Hello World” notebook:

<!DOCTYPE html>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/@observablehq/inspector@5/dist/inspector.css">
<body>
<script type="module">

import {Runtime, Inspector} from "https://cdn.jsdelivr.net/npm/@observablehq/runtime@5/dist/runtime.js";
import define from "https://api.observablehq.com/@observablehq/hello-world.js?v=4";

const runtime = new Runtime();
const main = runtime.module(define, name => {
  if (name === "hello") {
    const node = document.createElement("DIV");
    document.body.appendChild(node);
    return new Inspector(node);
  }
});

</script>

To render the entire notebook into the body, use Inspector.into:

const runtime = new Runtime();
const main = runtime.module(define, Inspector.into(document.body));

For more control, implement a custom observer in place of the standard inspector. The returned object may implement observer.pending, observer.fulfilled and observer.rejected methods to be notified when the corresponding variable changes state. For example:

const runtime = new Runtime();
const main = runtime.module(define, name => {
  return {
    pending() {
      console.log(`${name}: pending`);
    },
    fulfilled(value) {
      console.log(`${name}: fullfilled`, value);
    },
    rejected(error) {
      console.error(`${name}: rejected`, error);
    }
  };
});

Variables which are not associated with an observer, or aren’t indirectly depended on by a variable that is associated with an observer, will not be evaluated. To force a variable to be evaluated, return true. See module.variable.

API Reference

Runtimes

# new Runtime(builtins = new Library[, global]) <>

Returns a new runtime. If builtins is specified, each property on the builtins object defines a builtin variable for the runtime. These builtins are available as named inputs to any defined variables on any module associated with this runtime. If builtins is not specified, it defaults to the standard library. If a global function is specified, it will be invoked with the name of any unresolved reference, and must return the corresponding value or undefined (to trigger a ReferenceError); if global is not specified, unresolved values will be resolved from the global window.

Many Observable notebooks rely on the standard library builtins. To instead specify a custom set of builtins:

const runtime = new Runtime({color: "red"});

Or to define a new builtin, or override an existing one:

const runtime = new Runtime(Object.assign(new Library, {color: "red"}));

To refer to the color builtin from a variable:

const module = runtime.module();
const inspector = new Inspector(document.querySelector("#hello"));
module.variable(inspector).define(["color"], color => `Hello, ${color}.`);

This would produce the following output:

Hello, red.

Unlike variables, builtins cannot depend on the value of other variables or builtins; they are defined with no inputs. If a builtin is defined as a function, it will be invoked lazily to determine the value of the builtin. If you wish for the value of a builtin to be a function, the builtin must be defined either as a promise that resolves to a function or as a function that returns a function. Builtins may also be defined as generators for dynamic values; see now for example.

# runtime.module([define][, observer]) <>

Returns a new module for this runtime.

If define is specified, it is a function which defines the new module’s variables by calling runtime.module (with no arguments) and then calling module.variable on the returned module as desired. If this runtime already has a module for the specified define function, the existing module is returned; otherwise, a new module is created, and the define function is called, being passed this runtime and the specified observer factory function. If define is not specified, a new module is created and returned.

If an observer factory function is specified, it is called for each named variable in the returned module, being passed the variable’s name. The standard inspector is available as a ready-made observer: it displays DOM elements “as-is” and renders interactive displays for other arbitrary values such as numbers and objects.

# runtime.dispose() <>

Disposes this runtime, invalidating all active variables and disabling future computation.

Modules

A module is a namespace for variables; within a module, variables should typically have unique names. Imports allow variables to be referenced across modules.

# module.variable([observer]) <>

Returns a new variable for this module. The variable is initially undefined.

If observer is specified, the specified observer will be notified when the returned variable changes state, via the observer.pending, observer.fulfilled and observer.rejected methods. See the standard inspector for a convenient default observer implementation.

A variable without an associated observer is only computed if any transitive output of the variable has an observer; variables are computed on an as-needed basis for display. This is particularly useful when the runtime has multiple modules (as with imports): only the needed variables from imported modules are computed.

# module.derive(specifiers, source) <>

Returns a derived copy of this module, where each variable in specifiers is replaced by an import from the specified source module. The specifiers are specified as an array of objects with the following properties:

If specifier.alias is not specified, it defaults to specifier.name. A specifier may also be specified as a string, in which case the string is treated as both the name and the alias. For example, consider the following module which defines two constants a and b, and a variable c that represents their sum:

const module0 = runtime.module();
module0.variable().define("a", 1);
module0.variable().define("b", 2);
module0.variable().define("c", ["a", "b"], (a, b) => a + b);

To derive a new module that redefines b:

const module1 = runtime.module();
const module1_0 = module0.derive(["b"], module1);
module1.variable().define("b", 3);
module1.variable().import("c", module1_0);

The value of c in the derived module is now 1 + 3 = 4, whereas the value of c in the original module remains 1 + 2 = 3.

# module.define([name, ][inputs, ]definition) <>

A convenience method for variable.define; equivalent to:

module.variable().define(name, inputs, definition)

# module.import(name, [alias, ]from) <>

A convenience method for variable.import; equivalent to:

module.variable().import(name, alias, from)

# module.redefine(name[, inputs], definition) <>

Redefines the variable with the specified name on this module. If no such variable exists, or if more than one variable has the specified name, throws a runtime error.

# module.value(name) <>

Returns a promise to the next value of the variable with the specified name on this module. If no such variable exists, or if more than one variable has the specified name, throws a runtime error.

Variables

A variable defines a piece of state in a reactive program, akin to a cell in a spreadsheet. Variables may be named to allow the definition of derived variables: variables whose value is computed from other variables’ values. Variables are scoped by a module and evaluated by a runtime.

# variable.define([name, ][inputs, ]definition) <>

Redefines this variable to have the specified name, taking the variables with the names specified in inputs as arguments to the specified definition function. If name is null or not specified, this variable is anonymous and may not be referred to by other variables. The named inputs refer to other variables (possibly imported) in this variable’s module. Circular inputs are not allowed; the variable will throw a ReferenceError upon evaluation. If inputs is not specified, it defaults to the empty array. If definition is not a function, the variable is defined to have the constant value of definition.

The definition function may return a promise; derived variables will be computed after the promise resolves. The definition function may likewise return a generator; the runtime will pull values from the generator on every animation frame, or if the generator yielded a promise, after the promise is resolved. When the definition is invoked, the value of this is the variable’s previous value, or undefined if this is the first time the variable is being computed under its current definition. Thus, the previous value is preserved only when input values change; it is not preserved if the variable is explicitly redefined.

For example, consider the following module that starts with a single undefined variable, a:

const runtime = new Runtime(builtins);

const module = runtime.module();

const a = module.variable();

To define variable a with the name foo and the constant value 42:

a.define("foo", 42);

This is equivalent to:

a.define("foo", [], () => 42);

To define an anonymous variable b that takes foo as input:

const b = module.variable();

b.define(["foo"], foo => foo * 2);

This is equivalent to:

b.define(null, ["foo"], foo => foo * 2);

Note that the JavaScript symbols in the above example code (a and b) have no relation to the variable names (foo and null); variable names can change when a variable is redefined or deleted. Each variable corresponds to a cell in an Observable notebook, but the cell can be redefined to have a different name or definition.

If more than one variable has the same name at the same time in the same module, these variables’ definitions are temporarily overridden to throw a ReferenceError. When and if the duplicate variables are deleted, or are redefined to have unique names, the original definition of the remaining variable (if any) is restored. For example, here variables a and b will throw a ReferenceError:

const module = new Runtime(builtins).module();
const a = module.variable().define("foo", 1);
const b = module.variable().define("foo", 2);

If a or b is redefined to have a different name, both a and b will subsequently resolve to their desired values:

b.define("bar", 2);

Likewise deleting a or b would allow the other variable to resolve to its desired value.

# variable.import(name, [alias, ]module) <>

Redefines this variable as an alias of the variable with the specified name in the specified module. The subsequent name of this variable is the specified name, or if specified, the given alias. The order of arguments corresponds to the standard import statement: import {name as alias} from "module". For example, consider a module which defines a variable named foo:

const runtime = new Runtime(builtins);

const module0 = runtime.module();

module0.variable().define("foo", 42);

To import foo into another module:

const module1 = runtime.module();

module1.variable().import("foo", module0);

Now the variable foo is available to other variables in module1:

module1.variable().define(["foo"], foo => `Hello, ${foo}.`);

This would produce the following output:

Hello, 42.

To import foo into module1 under the alias bar:

module1.variable().import("foo", "bar", module0);

# variable.delete() <>

Deletes this variable’s current definition and name, if any. Any variable in this module that references this variable as an input will subsequently throw a ReferenceError. If exactly one other variable defined this variable’s previous name, such that that variable throws a ReferenceError due to its duplicate definition, that variable’s original definition is restored.

Observers

An observer watches a variable, being notified via asynchronous callback whenever the variable changes state. See the standard inspector for reference.

# observer.pending()

Called shortly before the variable is computed. For a generator variable, this occurs before the generator is constructed, but not before each subsequent value is pulled from the generator.

# observer.fulfilled(value)

Called shortly after the variable is fulfilled with a new value.

# observer.rejected(error)

Called shortly after the variable is rejected with the given error.

Library

For convenience, this module re-exports the Observable standard library.

Inspector

For convenience, this module re-exports the Observable standard inspector.