Public
Edited
Sep 14, 2022
Importers
1 star
Insert cell
Insert cell
Insert cell
Insert cell
{
// Create a new dynamic context
const context = newContext();

// Declare a new dynamic object:
const obj = {};
const props = context.defineProperties(obj, {
// Properties of the object
fields: {
firstName: "John",
lastName: "Smith",
age: 0
},
// Auto-calculated properties
computed: {
fullName: ({ firstName, lastName }) => firstName + " " + lastName,
message: ({ fullName, age }) => {
let msg = "young";
if (age < 5) msg = "a baby";
else if (age < 10) msg = "very young";
else if (age < 30) msg = "young";
else if (age < 50) msg = "in your best age";
else if (age < 70) msg = "sage";
else msg = "young again";
return `Hello, ${fullName}! You are ${age} years old and you are ${msg}!`;
}
}
});
context
.autorun(() => {
console.log("* ", obj.message);
})
.start();

// Now we will asynchronously change the age of our personage.
// The "message" field will be re-evaluated for each new "age" value.
(async function () {
let stop = false;
invalidation.then(() => (stop = true)); // Stop counting when this cell is re-evaluated.
for (let age = 1; !stop && age <= 100; age++) {
await Promises.delay(1000);
obj.age = age;
}
})();

// The returned generator provides greeting messages for each new year
return Generators.observe((update) => {
const [start, stop] = context.autorun(() => update(obj.message));
start();
return stop;
});
}
Insert cell
{
const context = newContext();
return context.newProperty(1);
}
Insert cell
Insert cell
function newContext(equals = (a, b) => a === b) {
const stack = [];
let peek;

function newId() {
return (newContext._idx = (newContext._idx || 0) + 1);
}
function stamp(l, force = false) {
return (l.id = (!force && l.id) || newId());
}

function newSlot(value, name) {
let actions = {},
list;
const s = [];
const id = stamp(s);
s.name = name;
s.get = s[0] = () => (peek && (peek[id] = s), value);
s.set = s[1] = (v) => {
let prev = value;
if (!equals(prev, v)) {
value = v;
const l = list || (list = Object.values(actions));
for (let i = 0; i < l.length; i++) {
l[i](s, prev);
}
}
};
s._get = s[2] = () => value;
s._set = s[3] = (v) => (value = v);
s.on = (a) => ((actions[stamp(a)] = a), (list = null), () => s.off(a));
s.off = (a) => (delete actions[stamp(a)], (list = null));
return s;
}

function startTracking(t = {}) {
stack.push((peek = t));
return t;
}

function stopTracking(t) {
if (!t) stack.pop();
else {
for (let idx = stack.length - 1; idx >= 0; idx--) {
if (t === stack[idx]) {
stack.splice(idx, 1);
break;
}
}
}
peek = stack[stack.length - 1];
}

function newTracker(onUpdate, filter) {
let dependencies, registrations;

function start() {
if (dependencies) return false;
if (registrations) registrations.forEach((unregister) => unregister());
registrations = undefined;
dependencies = startTracking();
return true;
}
function stop() {
if (!dependencies) return false;
stopTracking(dependencies);
let props = Object.values(dependencies);
if (filter) props = props.filter(filter);
registrations = props.map((prop) => prop.on(onUpdate));
dependencies = undefined;
return true;
}
function close() {
if (dependencies) stopTracking(dependencies);
if (registrations) registrations.forEach((unregister) => unregister());
dependencies = undefined;
registrations = undefined;
}

return Object.assign([start, stop, close], { start, stop, close });
}

function autorun(action, filter) {
const t = newTracker(update, filter);
function update() {
t.start();
try {
action();
} finally {
t.stop();
}
}
return Object.assign([update, t.close], { start: update, close: t.close });
}

function newComputedSlot(action, name, start = true) {
const slot = newSlot(undefined, name);
Object.assign(
slot,
autorun(
() => slot.set(action()),
(t) => t.id !== slot.id
)
);
start && slot.start();
return slot;
}

function newProperty(value, name) {
const slot = newSlot(value, name);
return Object.defineProperties(
{},
{
value: slot,
name: () => name,
_slot: () => slot
}
);
}

function newComputedProperty(action, name, start = true) {
const slot = newComputedSlot(action, name, start);
return Object.defineProperties(
{},
{
value: slot,
name: () => name,
_slot: () => slot
}
);
}

// ------------------------------------
// Utility methods to handle object fields

function defineProperty(obj, name, value) {
let slot = newSlot(value, name);
Object.defineProperty(obj, name, slot);
return slot;
}

function defineComputedProperty(obj, name, action) {
const slot = newComputedSlot(() => action.call(obj, obj), name, false);
Object.defineProperty(obj, name, { get: slot.get });
return slot;
}

function defineProperties(obj, { fields, computed = {}, start = true }) {
const slots = [];
slots.index = {};
for (let [field, value] of Object.entries(fields)) {
slots.push((slots.index[field] = defineProperty(obj, field, value)));
}
for (let [field, action] of Object.entries(computed)) {
slots.push(
(slots.index[field] = defineComputedProperty(obj, field, action))
);
}
if (start) slots.forEach((r) => r.start && r.start());
slots.get = (name) => slots.index[name];
return slots;
}

return {
autorun,
newTracker,
newProperty,
newComputedProperty,
defineProperty,
defineComputedProperty,
defineProperties,

// Private (internal) methods
newSlot,
newComputedSlot,
stamp,
startTracking,
stopTracking
};
}
Insert cell
function debounce(func, delay) {
let id;
return (...args) => (clearTimeout(id), id = setTimeout(() => func(...args), delay));
}
Insert cell
Insert cell
import { describe, expect, it } from '@kotelnikov/unit-tests-in-notebooks'
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more