van-hmr/lib/van/van.js

229 lines
7.6 KiB
JavaScript
Raw Normal View History

2025-02-03 14:13:41 -05:00
// This file consistently uses `let` keyword instead of `const` for reducing the bundle size.
// Global variables - aliasing some builtin symbols to reduce the bundle size.
let protoOf = Object.getPrototypeOf
let changedStates, derivedStates, curDeps, curNewDerives, alwaysConnectedDom = {isConnected: 1}
let gcCycleInMs = 1000, statesToGc, propSetterCache = {}
let objProto = protoOf(alwaysConnectedDom), funcProto = protoOf(protoOf), _undefined
let addAndScheduleOnFirst = (set, s, f, waitMs) =>
(set ?? (setTimeout(f, waitMs), new Set)).add(s)
let runAndCaptureDeps = (f, deps, arg) => {
let prevDeps = curDeps
curDeps = deps
try {
return f(arg)
} catch (e) {
console.error(e)
return arg
} finally {
curDeps = prevDeps
}
}
let keepConnected = l => l.filter(b => b._dom?.isConnected)
let addStatesToGc = d => statesToGc = addAndScheduleOnFirst(statesToGc, d, () => {
for (let s of statesToGc)
s._bindings = keepConnected(s._bindings),
s._listeners = keepConnected(s._listeners)
statesToGc = _undefined
}, gcCycleInMs)
let stateProto = {
get val() {
curDeps?._getters?.add(this)
return this.rawVal
},
get oldVal() {
curDeps?._getters?.add(this)
return this._oldVal
},
set val(v) {
curDeps?._setters?.add(this)
if (v !== this.rawVal) {
this.rawVal = v
this._bindings.length + this._listeners.length ?
(derivedStates?.add(this), changedStates = addAndScheduleOnFirst(changedStates, this, updateDoms)) :
this._oldVal = v
}
},
}
let state = initVal => ({
__proto__: stateProto,
rawVal: initVal,
_oldVal: initVal,
_bindings: [],
_listeners: [],
})
let bind = (f, dom) => {
let deps = {_getters: new Set, _setters: new Set}, binding = {f}, prevNewDerives = curNewDerives
curNewDerives = []
let newDom = runAndCaptureDeps(f, deps, dom)
newDom = (newDom ?? document).nodeType ? newDom : new Text(newDom)
for (let d of deps._getters)
deps._setters.has(d) || (addStatesToGc(d), d._bindings.push(binding))
for (let l of curNewDerives) l._dom = newDom
curNewDerives = prevNewDerives
return binding._dom = newDom
}
let derive = (f, s = state(), dom) => {
let deps = {_getters: new Set, _setters: new Set}, listener = {f, s}
listener._dom = dom ?? curNewDerives?.push(listener) ?? alwaysConnectedDom
s.val = runAndCaptureDeps(f, deps, s.rawVal)
for (let d of deps._getters)
deps._setters.has(d) || (addStatesToGc(d), d._listeners.push(listener))
return s
}
let add = (dom, ...children) => {
for (let c of children.flat(Infinity)) {
let protoOfC = protoOf(c ?? 0)
let child = protoOfC === stateProto ? bind(() => c.val) :
protoOfC === funcProto ? bind(c) : c
child != _undefined && dom.append(child)
}
return dom
}
let tag = (ns, name, ...args) => {
let [{is, ...props}, ...children] = protoOf(args[0] ?? 0) === objProto ? args : [{}, ...args]
let dom = ns ? document.createElementNS(ns, name, {is}) : document.createElement(name, {is})
for (let [k, v] of Object.entries(props)) {
let getPropDescriptor = proto => proto ?
Object.getOwnPropertyDescriptor(proto, k) ?? getPropDescriptor(protoOf(proto)) :
_undefined
let cacheKey = name + "," + k
let propSetter = propSetterCache[cacheKey] ??= getPropDescriptor(protoOf(dom))?.set ?? 0
let setter = k.startsWith("on") ?
(v, oldV) => {
let event = k.slice(2)
dom.removeEventListener(event, oldV)
dom.addEventListener(event, v)
} :
propSetter ? propSetter.bind(dom) : dom.setAttribute.bind(dom, k)
let protoOfV = protoOf(v ?? 0)
k.startsWith("on") || protoOfV === funcProto && (v = derive(v), protoOfV = stateProto)
protoOfV === stateProto ? bind(() => (setter(v.val, v._oldVal), dom)) : setter(v)
}
return add(dom, children)
}
let handler = ns => ({get: (_, name) => tag.bind(_undefined, ns, name)})
let update = (dom, newDom) => newDom ? newDom !== dom && dom.replaceWith(newDom) : dom.remove()
2025-02-03 14:54:44 -05:00
//let updateDoms = () => {
// let iter = 0, derivedStatesArray = [...changedStates].filter(s => s.rawVal !== s._oldVal)
// do {
// derivedStates = new Set
// for (let l of new Set(derivedStatesArray.flatMap(s => s._listeners = keepConnected(s._listeners))))
// derive(l.f, l.s, l._dom), l._dom = _undefined
// } while (++iter < 100 && (derivedStatesArray = [...derivedStates]).length)
// let changedStatesArray = [...changedStates].filter(s => s.rawVal !== s._oldVal)
// changedStates = _undefined
// for (let b of new Set(changedStatesArray.flatMap(s => s._bindings = keepConnected(s._bindings))))
// update(b._dom, bind(b.f, b._dom)), b._dom = _undefined
// for (let s of changedStatesArray) s._oldVal = s.rawVal
//}
/*********************************************** */
// Map to track state-to-DOM relationships
const stateDomMap = new Map();
function pathHash(/** @type {HTMLElement} */element) {
console.log(element);
const path = [];
let currentElement = element;
while (currentElement && currentElement !== document.body && currentElement.parentNode) {
const siblings = Array.from(currentElement.parentNode.children);
const index = siblings.indexOf(currentElement);
path.unshift(index); // Push the index of the current element at the start of the path
currentElement = currentElement.parentNode;
}
// Include the body in the path as the final part, with index 0 (since it's the root)
path.unshift(0);
return path.join('-'); // Return the path as a string, e.g., "1-0-0-2"
}
/**
* Logs a relationship between a state and a DOM element.
* @param {any} state - The state object being tracked.
* @param {Element} dom - The DOM element affected by the state.
*/
const trackStateDomRelationship = (state, dom, thing) => {
console.log(state, thing);
const hash = pathHash(dom);
if (!stateDomMap.has(hash)) {
stateDomMap.set(hash, new Set());
}
stateDomMap.get(hash).add(state);
};
// Enhanced updateDoms function
2025-02-03 14:13:41 -05:00
let updateDoms = () => {
2025-02-03 14:54:44 -05:00
let iter = 0;
let derivedStatesArray = [...changedStates].filter(s => s.rawVal !== s._oldVal);
2025-02-03 14:13:41 -05:00
do {
2025-02-03 14:54:44 -05:00
// Clear derivedStates for this iteration
derivedStates = new Set();
// Process listeners for derived states
for (let l of new Set(derivedStatesArray.flatMap(s => s._listeners = keepConnected(s._listeners)))) {
derive(l.f, l.s, l._dom);
// Track which DOM element is being touched by this listener's state
if (l._dom) trackStateDomRelationship(l.s, l._dom);
l._dom = _undefined;
}
} while (++iter < 100 && (derivedStatesArray = [...derivedStates]).length);
// Update bindings for changed states
let changedStatesArray = [...changedStates].filter(s => s.rawVal !== s._oldVal);
changedStates = _undefined;
for (let b of new Set(changedStatesArray.flatMap(s => s._bindings = keepConnected(s._bindings)))) {
// Track which DOM element is being touched by this binding's state
if (b._dom) trackStateDomRelationship(b.f, b._dom);
update(b._dom, bind(b.f, b._dom));
b._dom = _undefined;
}
// Update old values for changed states
for (let s of changedStatesArray) {
s._oldVal = s.rawVal;
}
};
// Debugging helper to log the state-DOM relationships
globalThis.Divulge = () => {
console.log("State-DOM Relationships:");
for (const [state, domSet] of stateDomMap.entries()) {
console.log(state, [...domSet]);
}
};
globalThis.path = pathHash;
/*********************************************** */
2025-02-03 14:13:41 -05:00
export default {
tags: new Proxy(ns => new Proxy(tag, handler(ns)), handler()),
hydrate: (dom, f) => update(dom, bind(f, dom)),
add, state, derive,
}