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
}
);
}
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,
newSlot,
newComputedSlot,
stamp,
startTracking,
stopTracking
};
}