@observablehq/notebook-runtime

[![CircleCI](https://circleci.com/gh/observablehq/notebook-runtime/tree/master.svg?style=svg&circle-token=765ad8079db8d24462864a9ed0ec5eab25404918)](https://circleci.com/gh/observablehq/notebook-runtime/tree/master)

Downloads in past

Stats

StarsIssuesVersionUpdatedCreatedSize
@observablehq/notebook-runtime
95952.0.05 years ago6 years agoMinified + gzip package size for @observablehq/notebook-runtime in KB

Readme

@observablehq/notebook-runtime
CircleCI
This library implements the reactive runtime for Observable notebooks. It lets you publish your interactive notebooks wherever you want: on your website, integrated into your web application or interactive dashboard — to any distant shore the web platform reaches. You can also use this library to author reactive programs by hand, to build new reactive editors, or simply to better understand how the Observable runtime works.

API Reference

Runtimes

# Runtime.load(notebook, builtins, observer) <>
Creates, initializes and returns a new runtime for the given notebook definition.
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. See Runtime. If builtins is not specified, it defaults to the standard library.
The observer function is called for each (non-import) variable in the main module, being passed the variable, its index, and the list of variables.
Inspector is included as a ready-made observer. Inspectors display DOM elements “as-is”, and create interactive “devtools”-style inspectors for other arbitrary values such as numbers and objects.
This example creates an inspector for each named cell in a Hello world notebook:
<div id=hello></div>
<script type=module>
import {Runtime, Inspector} from "https://unpkg.com/@observablehq/notebook-runtime@1?module";
import notebook from "https://api.observablehq.com/@tmcw/hello-world.js?key=1fa3950f61d6e560";

Runtime.load(notebook, variable => {
  return new Inspector(document.getElementById(variable.name));
});
</script>

You can also create custom observers. The returned observer may implement observer.pending, observer.fulfilled and observer.rejected methods to be notified when the corresponding variable changes state. For example:
import {Runtime} from "https://unpkg.com/@observablehq/notebook-runtime@1?module";
import notebook from "https://api.observablehq.com/@tmcw/hello-world.js?key=1fa3950f61d6e560";

Runtime.load(notebook, variable => {
  const node = document.getElementById(variable.name);
  return {
    pending: () => {
      node.classList.add("running")
    },
    fulfilled: (value) => {
      node.classList.remove("running");
      node.innerText = value;
    },
    rejected: (error) => {
      node.classList.remove("running");
      node.classList.add("error");
      node.innerText = error.message;
    }
  };
});

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. See module.variable.

The notebook format

Notebooks passed to Runtime.load() are objects with notebook.id and notebook.modules properties:
const notebook = {
  id: "7d0eb6673a55a7c@3",
  modules: [
    {
      id: "7d0eb6673a55a7c@3",
      variables: [
        {
          name: "title",
          value: function() {
            return "Hello, world!"
          }
        }
      ]
    }
  ]
};

The notebook may contain multiple modules, each defining reactive variables. When the main module contains imports, the notebook bundles all of its resolved dependencies. For example:
const notebook = {
  id: "2710b07ba2cc1a8a@5",
  modules: [
    {
      id: "2710b07ba2cc1a8a@5",
      variables: [
        {
          from: "904bc713463f843@7",
          name: "foo",
          remote: "foo"
        }
      ]
    },
    {
      id: "904bc713463f843@7",
      variables: [
        {
          name: "foo",
          inputs: [],
          value: function() {
            return 42;
          }
        }
      ]
    }
  ]
};

# new Runtime(builtins, global) <>
Returns a new runtime. 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 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.
Most notebooks created in Observable rely on the standard library builtins, which are available as the Library class. For example, to create a runtime that includes standard library builtins like now and width:
import {Runtime, Library} from "https://unpkg.com/@observablehq/notebook-runtime@1?module";
const runtime = new Runtime(new Library);

You can instead specify an entirely custom set of builtins:
const runtime = new Runtime({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.

Builtins must have constant values; unlike variables, they cannot be defined as functions. However, a builtin may be defined as a promise, in which case any referencing variables will be evaluated only after the promise is resolved. Variables may not override builtins.
# runtime.module() <>
Returns a new module for this runtime.

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:
  • specifier.name - the name of the variable to import from source.
  • specifier.alias - the name of the variable to redefine in this module.

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)

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 includes and exports the Library class from notebook-stdlib, so that notebooks can easily use the standard library. Typically the Library class will be constructed and passed as the builtins argument to Runtime.
Refer to the notebook-stdlib module for full API documentation.

Inspector

For convenience, this module includes and exports the Inspector
class from notebook-inspector, so that notebooks can easily use the default inspector.
A simple 'Hello world' example:
import {Runtime, Inspector, Library} from "@observablehq/notebook-runtime";

const runtime = new Runtime(new Library);
const module = runtime.module();

module.variable(new Inspector(document.querySelector("#hello"))).define(() => `Hello world`);

Refer to the notebook-inspector module for full API documentation.