227 lines
7.2 KiB
JavaScript
227 lines
7.2 KiB
JavaScript
// 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
|
|
//}
|
|
/*********************************************** */
|
|
const Gateway = {
|
|
Time: 0,
|
|
Temp:{},
|
|
Tick()
|
|
{
|
|
for(let k in Gateway.Temp)
|
|
{
|
|
localStorage.setItem(k, Gateway.Temp[k]);
|
|
}
|
|
Gateway.Temp = {};
|
|
Gateway.Time = 0;
|
|
},
|
|
Save(key, value)
|
|
{
|
|
Gateway.Temp[key] = value;
|
|
if(!Gateway.Time)
|
|
{
|
|
Gateway.Time = setTimeout(Gateway.Tick, 500);
|
|
}
|
|
},
|
|
Load(key)
|
|
{
|
|
return localStorage.getItem(key);
|
|
}
|
|
}
|
|
|
|
function pathHash(/** @type {HTMLElement} */element)
|
|
{
|
|
const path = [];
|
|
while (element && element !== document.body) {
|
|
|
|
const parent = element.parentElement
|
|
path.unshift(Array.from(parent.children).indexOf(element)); // Push the index of the current element at the start of the path
|
|
element = parent;
|
|
}
|
|
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, doms) => {
|
|
doms.forEach(dom => {
|
|
if(dom)
|
|
{
|
|
const hash = pathHash(dom);
|
|
Gateway.Save(hash, JSON.stringify(state));
|
|
}
|
|
});
|
|
};
|
|
|
|
// Enhanced updateDoms function
|
|
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
|
|
|
|
const _block = new Set(changedStatesArray.flatMap(s =>{
|
|
trackStateDomRelationship(s.rawVal, s._bindings.map(b=>b._dom));
|
|
s.rawVal = "FORCE";
|
|
return s._bindings = keepConnected(s._bindings);
|
|
}));
|
|
|
|
for (let b of _block) {
|
|
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(Gateway.Temp);
|
|
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,
|
|
} |