Skip to content

Instantly share code, notes, and snippets.

@jamesdiacono
Created October 12, 2025 06:20
Show Gist options
  • Save jamesdiacono/c52e9f02b1ffd81364de6ffabe076ea6 to your computer and use it in GitHub Desktop.
Save jamesdiacono/c52e9f02b1ffd81364de6ffabe076ea6 to your computer and use it in GitHub Desktop.
Reactivity library
// "Spreadsheet" is a minimal reactive programming library for JavaScript.
// A spreadsheet is made up of cells, each holding a single value. A cell can be
// watched, such that a callback is invoked whenever its value changes.
// Breaking from tradition, cells are not arranged in rows and columns with
// predictable addresses. Instead, a cell can be accessed only if its object
// reference is known. This makes it possible to safely partition a spreadsheet
// across trust boundaries. Untrusted code can be given access only to those
// cells needed to do its work.
// This module exports a function that makes spreadsheets. A spreadsheet
// consists of two functions, 'cell' and 'watch'.
// import make_spreadsheet from "./spreadsheet.js";
// const {cell, watch} = make_spreadsheet();
// The 'cell' function makes cells. Its argument is the cell's initial value. A
// cell is an object consisting of a 'read' and a 'write' function.
// const {read, write} = cell("mango");
// The value of the cell is held in a closure. The value can only be read by
// calling the 'read' function.
// read(); // "mango"
// The value is overwritten by calling the 'write' function with the new value.
// write("pear"); // undefined
// read(); // "pear"
// The 'watch' function is used to monitor cell mutation. It takes a watcher
// function as its argument, which is called immediately. A stop function is
// returned.
// During each invocation of the watcher, every cell that gets read has its
// reference retained. Thereafter, the watcher is invoked whenever the value of
// one of these cells changes.
// function watcher() {
// console.log(read());
// }
// const stop = watch(watcher); // Logs "pear".
// write("apple"); // Logs "apple".
// Deep mutation of a cell's value is not detected.
// write(["passionfruit"]); // Logs ["passionfruit"].
// read().push("grape"); // Nothing is logged.
// That is why it is better for cells to hold immutable values.
// write(Object.freeze(["passionfruit"])); // Logs ["passionfruit"].
// read().push("grape"); // Exception!
// Call the stop function to dispose of the watcher.
// stop();
// write("grape"); // Nothing is logged.
// The 'watch' function of one spreadsheet is powerless to watch the cells of
// another spreadsheet. This makes it possible to pass cells across trust
// boundaries without exposing their reactivity.
function equal(a, b) {
return a === b || (Number.isNaN(a) && Number.isNaN(b));
}
function make_spreadsheet(on_exception) {
// The 'on_exception' parameter is a function that is called with an exception
// whenever a watcher function fails. It must not throw. It may be omitted
// during development, but should always be provided in production. Otherwise,
// the entire spreadsheet is corrupted when a watcher throws.
let subscription_arrays = new WeakMap();
let stopped_watchers = new WeakMap();
let running_watchers = [];
function run(watcher) {
// The 'run' function invokes the 'watcher' function.
running_watchers.push(watcher);
if (on_exception === undefined) {
watcher();
} else {
// If the watcher fails, an exception is reported but control is not
// transferred. If control was transferred, the 'running_watchers' array would
// be corrupted and hence the 'on_read' function would start malfunctioning
// wildly.
try {
watcher();
} catch (exception) {
on_exception(exception);
}
}
running_watchers.pop();
}
function on_read(cell) {
// The 'on_read' function is called immediately before a 'cell' is read. If
// there is a watcher currently executing, it is subscribed to the 'cell'. This
// is how we discover what cells are accessed by a watcher.
const now_watcher = running_watchers[running_watchers.length - 1];
if (now_watcher !== undefined) {
const watchers = subscription_arrays.get(cell) || [];
if (!watchers.includes(now_watcher)) {
watchers.push(now_watcher);
}
subscription_arrays.set(cell, watchers);
}
}
function unstopped(watcher) {
return !stopped_watchers.has(watcher);
}
function on_write(cell) {
// The 'on_write' function is called immediately after the value in 'cell' has
// changed. It runs every watcher that is subscribed to the cell.
const watchers = subscription_arrays.get(cell) || [];
watchers.filter(unstopped).forEach(run);
}
function watch(watcher) {
// The 'watch' function runs the watcher immediately.
run(watcher);
return function stop() {
// We are not able to remove the watcher from the subscription arrays. These
// arrays are inaccessible without the object reference of their corresponding
// cell, which is not available here. The best we can do is mark the watcher as
// stopped.
stopped_watchers.set(watcher, true);
};
}
function cell(value) {
const the_cell = Object.create(null);
function read() {
on_read(the_cell);
return value;
}
function write(new_value) {
if (!equal(new_value, value)) {
value = new_value;
on_write(the_cell);
}
}
// We deep freeze the cell, making its interface immutable. The only way it can
// be used for communication is by invoking its methods.
the_cell.read = Object.freeze(read);
the_cell.write = Object.freeze(write);
return Object.freeze(the_cell);
}
return Object.freeze({watch, cell});
}
if (import.meta.main) {
const {cell, watch} = make_spreadsheet(console.error);
const numerator = cell(1);
const denominator = cell(1);
function division() {
return numerator.read() / denominator.read();
}
const stop = watch(function watcher() {
console.log(division());
}); // 1 / 1 === 1
numerator.write(0); // 0 / 1 === 0
denominator.write(0); // 0 / 0 === NaN
numerator.write(3); // 3 / 0 === Infinity
denominator.write(2); // 3 / 2 === 1.5
stop();
denominator.write(1);
}
export default Object.freeze(make_spreadsheet);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment