Created
October 12, 2025 06:20
-
-
Save jamesdiacono/c52e9f02b1ffd81364de6ffabe076ea6 to your computer and use it in GitHub Desktop.
Reactivity library
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // "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