// 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() //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 let updateDoms = () => { let iter = 0; let derivedStatesArray = [...changedStates].filter(s => s.rawVal !== s._oldVal); do { // 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; /*********************************************** */ export default { tags: new Proxy(ns => new Proxy(tag, handler(ns)), handler()), hydrate: (dom, f) => update(dom, bind(f, dom)), add, state, derive, }