diff --git a/sw.js b/sw.js index f595c8e..11b9916 100644 --- a/sw.js +++ b/sw.js @@ -1,15 +1,15 @@ -/* -* Fixed: Memory Leaks, Fragment Lifecycle, and List Reconciler. -* Refactored: Compact & Descriptive naming. -*/ -// --- Utilities --- -const isFunction = (v) => typeof v === 'function', isNode = (v) => v instanceof Node; +const isFunction = (value) => typeof value === 'function'; +const isNode = (value) => value instanceof Node; // --- Schedule System --- let isScheduled = false; const updateQueue = new Set(); -const processQueue = () => { updateQueue.forEach(cb => cb()); updateQueue.clear(); isScheduled = false; }; +const processQueue = () => { + updateQueue.forEach(callback => callback()); + updateQueue.clear(); + isScheduled = false; +} // --- Effects System --- let activeEffect = null; @@ -17,193 +17,309 @@ export const effect = (fn, isScope = false) => { let cleanup = null; const run = () => { stop(); - const prev = activeEffect; activeEffect = run; - try { cleanup = fn(); } finally { activeEffect = prev; } - }; + const previousEffect = activeEffect; + activeEffect = run; + try { cleanup = fn(); } finally { activeEffect = previousEffect; } + } const stop = () => { - run.subscriptions.forEach(subs => subs.delete(run)); run.subscriptions.clear(); + run.subscriptions.forEach(subscribers => subscribers.delete(run)); + run.subscriptions.clear(); if (isFunction(cleanup)) cleanup(); - if (run.childEffects) { run.childEffects.forEach(s => s()); run.childEffects.length = 0; } - }; - run.subscriptions = new Set(); if (isScope) run.childEffects = []; - run(); if (activeEffect?.childEffects) activeEffect.childEffects.push(stop); + if (run.childEffects) { + run.childEffects.forEach(stopChild => stopChild()); + run.childEffects.length = 0; + } + } + run.subscriptions = new Set(); + if (isScope) run.childEffects = []; + run(); + if (activeEffect?.childEffects) activeEffect.childEffects.push(stop); return stop; -}; +} export const scope = (fn) => effect(fn, true); -const track = (subs) => { if (activeEffect && !activeEffect.childEffects) { subs.add(activeEffect); activeEffect.subscriptions.add(subs); } }; + +const track = (subscribers) => { + if (activeEffect && !activeEffect.childEffects) { + subscribers.add(activeEffect); + activeEffect.subscriptions.add(subscribers); + } +} // --- Signals Core --- -export const signal = (val, key = null) => { - const subs = new Set(), storage = typeof localStorage !== 'undefined'; - let current = val; - if (key && storage) { const saved = localStorage.getItem(key); if (saved !== null) try { current = JSON.parse(saved); } catch {} } +export const signal = (initialValue, storageKey = null) => { + const subscribers = new Set(); + const hasStorage = typeof localStorage !== 'undefined'; + let currentValue = initialValue; - const sig = { + if (storageKey && hasStorage) { + const saved = localStorage.getItem(storageKey); + if (saved !== null) try { currentValue = JSON.parse(saved); } catch {} + } + + const signalObject = { _isSignal: true, - get value() { track(subs); return current; }, - set value(v) { - if (v === current) return; - current = v; subs.forEach(cb => updateQueue.add(cb)); + get value() { track(subscribers); return currentValue; }, + set value(newValue) { + if (newValue === currentValue) return; + currentValue = newValue; + subscribers.forEach(callback => updateQueue.add(callback)); if (!isScheduled) { isScheduled = true; queueMicrotask(processQueue); } } }; - if (key && storage) effect(() => localStorage.setItem(key, JSON.stringify(sig.value))); - return sig; + + if (storageKey && hasStorage) { + effect(() => localStorage.setItem(storageKey, JSON.stringify(signalObject.value))); + } + + return signalObject; }; -export const untrack = (fn) => { const prev = activeEffect; activeEffect = null; const res = fn(); activeEffect = prev; return res; }; -export const computed = (fn) => { const sig = signal(); effect(() => sig.value = fn()); return { get value() { return sig.value; } }; }; +export const untrack = (fn) => { + const previousEffect = activeEffect; + activeEffect = null; + const result = fn(); + activeEffect = previousEffect; + return result; +} + +export const computed = (fn) => { + const sig = signal(); + effect(() => sig.value = fn()); + return { get value() { return sig.value; } }; +} const reactiveCache = new WeakMap(); -export const reactive = (obj) => { - if (reactiveCache.has(obj)) return reactiveCache.get(obj); - const subsMap = {}; - const proxy = new Proxy(obj, { - get(t, k) { track(subsMap[k] ??= new Set()); const v = t[k]; return (v && typeof v === 'object') ? reactive(v) : v; }, - set(t, k, v) { - if (t[k] === v) return true; - t[k] = v; if (subsMap[k]) { subsMap[k].forEach(cb => updateQueue.add(cb)); if (!isScheduled) { isScheduled = true; queueMicrotask(processQueue); } } +export const reactive = (targetObject) => { + if (reactiveCache.has(targetObject)) return reactiveCache.get(targetObject); + const subscribersMap = {}; + const proxy = new Proxy(targetObject, { + get(target, key) { + track(subscribersMap[key] ??= new Set()); + const value = target[key]; + return (value && typeof value === 'object') ? reactive(value) : value; + }, + set(target, key, value) { + if (target[key] === value) return true; + target[key] = value; + if (subscribersMap[key]) { + subscribersMap[key].forEach(callback => updateQueue.add(callback)); + if (!isScheduled) { isScheduled = true; queueMicrotask(processQueue); } + } return true; } }); - reactiveCache.set(obj, proxy); return proxy; -}; + reactiveCache.set(targetObject, proxy); + return proxy; +} -export const watch = (src, cb) => { - let first = true, old; +export const watch = (source, callback) => { + let isFirstRun = true, oldValue; return effect(() => { - const val = isFunction(src) ? src() : src.value; - if (!first) untrack(() => cb(val, old)); else first = false; - old = val; + const newValue = isFunction(source) ? source() : source.value; + if (!isFirstRun) untrack(() => callback(newValue, oldValue)); + else isFirstRun = false; + oldValue = newValue; }); -}; +} // --- Rendering System --- let currentContext = null; export const onMount = (fn) => currentContext?.mountHooks.push(fn); export const onUnmount = (fn) => currentContext?.unmountHooks.push(fn); -export const provide = (k, v) => currentContext && (currentContext.providers[k] = v); -export const inject = (k, dft) => currentContext && (k in currentContext.providers ? currentContext.providers[k] : dft); +export const provide = (key, value) => currentContext && (currentContext.providers[key] = value); +export const inject = (key, defaultValue) => currentContext && (key in currentContext.providers ? currentContext.providers[key] : defaultValue); const remove = (node) => { if (Array.isArray(node)) return node.forEach(remove); - node.$stopEffect?.(); if (node.$context) node.$context.unmountHooks.forEach(h => h()); - const done = () => node.remove(); - node.$leave ? node.$leave(done) : done(); -}; + node.$stopEffect?.(); + if (node.$context) node.$context.unmountHooks.forEach(hook => hook()); + const finalize = () => node.remove(); + node.$leaveTransition ? node.$leaveTransition(finalize) : finalize(); +} -const render = (fn, ...data) => { - let node; const stop = effect(() => { node = fn(...data); if (isFunction(node)) node = node(); }, true); - if (node) node.$stopEffect = stop; return node; -}; +const render = (renderFn, ...data) => { + let node; + const stop = effect(() => { + node = renderFn(...data); + if (isFunction(node)) node = node(); + }, true); + if (node) node.$stopEffect = stop; + return node; +} export const h = (tag, props = {}, ...children) => { children = children.flat(Infinity); + if (isFunction(tag)) { - const prev = currentContext; - currentContext = { mountHooks: [], unmountHooks: [], providers: { ...(prev?.providers || {}) } }; - const ctx = currentContext; let el; + const previousContext = currentContext; + currentContext = { mountHooks: [], unmountHooks: [], providers: { ...(previousContext?.providers || {}) } }; + const localContext = currentContext; + let element; const stop = effect(() => { - el = tag(props, { children, emit: (e, ...a) => props[`on${e[0].toUpperCase()}${e.slice(1)}`]?.(...a) }); - return () => ctx.unmountHooks.forEach(h => h()); + element = tag(props, { + children, + emit: (event, ...args) => props[`on${event[0].toUpperCase()}${event.slice(1)}`]?.(...args) + }); + return () => localContext.unmountHooks.forEach(hook => hook()); }, true); - const out = isNode(el) ? el : document.createTextNode(String(el)); - out.$context = ctx; out.$stopEffect = stop; currentContext = prev; return out; + + const output = isNode(element) ? element : document.createTextNode(String(element)); + output.$context = localContext; + output.$stopEffect = stop; + currentContext = previousContext; + return output; } + if (!tag) return children; - const el = ['svg', 'path', 'circle'].includes(tag) ? document.createElementNS("http://www.w3.org/2000/svg", tag) : document.createElement(tag); - for (const k in props) { - if (k.startsWith('on')) el.addEventListener(k.slice(2).toLowerCase(), props[k]); - else if (k === "ref") isFunction(props[k]) ? props[k](el) : props[k].value = el; - else if (isFunction(props[k])) effect(() => el[k] = props[k]()); - else el[k] = props[k]; + + const isSvg = ['svg', 'path', 'circle'].includes(tag); + const element = isSvg + ? document.createElementNS("http://www.w3.org/2000/svg", tag) + : document.createElement(tag); + + for (const key in props) { + if (key.startsWith('on')) element.addEventListener(key.slice(2).toLowerCase(), props[key]); + else if (key === "ref") isFunction(props[key]) ? props[key](element) : props[key].value = element; + else if (isFunction(props[key])) effect(() => element[key] = props[key]()); + else element[key] = props[key]; } - children.forEach(c => append(el, c)); return el; -}; + + children.forEach(child => append(element, child)); + return element; +} const append = (parent, child) => { if (child == null) return; if (isFunction(child)) { - const anchor = document.createTextNode(''); parent.appendChild(anchor); - let nodes = []; + const anchor = document.createTextNode(''); + parent.appendChild(anchor); + let currentNodes = []; effect(() => { - const raw = [child()].flat(Infinity).filter(n => n != null); - const next = raw.map(n => isNode(n) ? n : document.createTextNode(String(n))); - nodes.forEach(n => { if (!next.includes(n)) remove(n); }); - next.forEach((n, i) => { if (!nodes.includes(n)) { parent.insertBefore(n, next[i+1] || anchor); if (n.$context) n.$context.mountHooks.forEach(h => h()); } }); - nodes = next; + const rawChildren = [child()].flat(Infinity).filter(node => node != null); + const nextNodes = rawChildren.map(node => isNode(node) ? node : document.createTextNode(String(node))); + currentNodes.forEach(node => { if (!nextNodes.includes(node)) remove(node); }); + nextNodes.forEach((node, index) => { + if (!currentNodes.includes(node)) { + parent.insertBefore(node, nextNodes[index + 1] || anchor); + if (node.$context) node.$context.mountHooks.forEach(hook => hook()); + } + }); + currentNodes = nextNodes; }, true); - } else parent.appendChild(isNode(child) ? child : document.createTextNode(String(child))); -}; + } else { + parent.appendChild(isNode(child) ? child : document.createTextNode(String(child))); + } +} -// --- Components & Router --- -export const If = (cond, renderFn, fallback = null) => { - let cached, current; +// --- Control Flow & Built-in Components --- +export const If = (condition, renderFn, fallback = null) => { + let cachedNode, currentCondition; return () => { - const show = !!cond(); - if (show !== current) { if (cached) remove(cached); cached = show ? render(renderFn) : (isFunction(fallback) ? render(fallback) : fallback); current = show; } - return cached; - }; -}; + const show = !!condition(); + if (show !== currentCondition) { + if (cachedNode) remove(cachedNode); + cachedNode = show ? render(renderFn) : (isFunction(fallback) ? render(fallback) : fallback); + currentCondition = show; + } + return cachedNode; + } +} export const For = (list, keyFn, renderFn) => { - let cache = new Map(); + let nodeCache = new Map(); return () => { - const next = new Map(), items = isFunction(list) ? list() : (list.value || list); - const res = items.map((item, i) => { - const id = isFunction(keyFn) ? keyFn(item, i) : (keyFn ? item[keyFn] : item); - let node = cache.get(id) || render(renderFn, item, i); - next.set(id, node); return node; + const nextCache = new Map(); + const items = isFunction(list) ? list() : (list.value || list); + const results = items.map((item, index) => { + const id = isFunction(keyFn) ? keyFn(item, index) : (keyFn ? item[keyFn] : item); + let node = nodeCache.get(id); + if (!node) node = render(renderFn, item, index); + nextCache.set(id, node); + return node; }); - cache.forEach((n, id) => { if (!next.has(id)) remove(n); }); cache = next; return res; - }; -}; + nodeCache.forEach((node, id) => { if (!nextCache.has(id)) remove(node); }); + nodeCache = nextCache; + return results; + } +} -export const Router = (routes) => { - const path = signal(window.location.hash.slice(1) || '/'); - window.onhashchange = () => path.value = window.location.hash.slice(1) || '/'; - return h('div', { class: 'router-view' }, () => { - const cur = path.value; - for (const r of routes) { - const match = cur.match(new RegExp(`^${r.path.replace(/:[^\s/]+/g, '([^/]+)')}$`)); - if (match) { - const params = {}; (r.path.match(/:[^\s/]+/g) || []).forEach((k, i) => params[k.slice(1)] = match[i+1]); - return h(r.component, { params, path: cur }); +export const Component = ({ is, ...props }, { children }) => () => h(isFunction(is) ? is() : is, props, children); + +export const Transition = ({ enter, idle, leave }, { children: [child] }) => { + const decorate = (element) => { + if (!isNode(element)) return element; + const addClasses = css => css && element.classList.add(...css.split(' ')); + const removeClasses = css => css && element.classList.remove(...css.split(' ')); + + if (enter) { + requestAnimationFrame(() => { + addClasses(enter[1]); + requestAnimationFrame(() => { + addClasses(enter[0]); removeClasses(enter[1]); addClasses(enter[2]); + element.addEventListener('transitionend', () => { + removeClasses(enter[2]); removeClasses(enter[0]); addClasses(idle); + }, { once: true }); + }); + }); + } + if (leave) { + element.$leaveTransition = (done) => { + removeClasses(idle); addClasses(leave[1]); + requestAnimationFrame(() => { + addClasses(leave[0]); removeClasses(leave[1]); addClasses(leave[2]); + element.addEventListener('transitionend', () => { + removeClasses(leave[2]); removeClasses(leave[0]); done(); + }, { once: true }); + }); } } - const fbk = routes.find(r => r.path === '*'); return fbk ? h(fbk.component) : '404'; + return element; + } + return isFunction(child) ? () => decorate(child()) : decorate(child); +} + +// --- Routing & Application Entry --- +export const Router = (routes) => { + const currentPath = signal(window.location.hash.slice(1) || '/'); + window.onhashchange = () => currentPath.value = window.location.hash.slice(1) || '/'; + return h('div', { class: 'router-view' }, () => { + const pathValue = currentPath.value; + for (const route of routes) { + const pattern = new RegExp(`^${route.path.replace(/:[^\s/]+/g, '([^/]+)')}$`); + const match = pathValue.match(pattern); + if (match) { + const params = {}; + const keys = route.path.match(/:[^\s/]+/g) || []; + keys.forEach((key, index) => params[key.slice(1)] = match[index + 1]); + return h(route.component, { params, path: pathValue }); + } + } + const fallback = routes.find(r => r.path === '*'); + return fallback ? h(fallback.component) : '404'; }); }; -export const Transition = ({ enter: e, idle, leave: l }, { children: [c] }) => { - const decorate = (el) => { - if (!isNode(el)) return el; - const cls = (css, add = true) => css && el.classList[add ? 'add' : 'remove'](...css.split(' ')); - if (e) { - cls(e[1]); - requestAnimationFrame(() => { - cls(e[0]); cls(e[2]); cls(e[1], false); - el.addEventListener('transitionend', () => { cls(e[2], false); cls(e[0], false); cls(idle); }, { once: true }); - }); - } - if (l) el.$leave = (done) => { - cls(idle, false); cls(l[1]); - requestAnimationFrame(() => { - cls(l[0]); cls(l[2]); cls(l[1], false); - el.addEventListener('transitionend', () => { cls(l[2], false); cls(l[0], false); done(); }, { once: true }); - }); - }; - return el; - }; - return isFunction(c) ? () => decorate(c()) : decorate(c); +export const mount = (rootComponent, target, props = {}) => { + const destination = typeof target === 'string' ? document.querySelector(target) : target; + if (!destination) return; + + while (destination.firstChild) remove(destination.firstChild); + + const element = h(rootComponent, props); + destination.appendChild(element); + + if (element.$context) { + element.$context.mountHooks.forEach(hook => hook()); + element.$context.mountHooks.length = 0; + } + + return () => remove(element); }; -export const mount = (root, target, props = {}) => { - const dest = typeof target === 'string' ? document.querySelector(target) : target; - if (!dest) return; - while (dest.firstChild) remove(dest.firstChild); - const el = h(root, props); dest.appendChild(el); - if (el.$context) { el.$context.mountHooks.forEach(h => h()); el.$context.mountHooks.length = 0; } - return () => remove(el); -}; \ No newline at end of file +export default (target, rootComponent, props) => { + const element = h(rootComponent, props); + target.appendChild(element); + if (element.$context) element.$context.mountHooks.forEach(hook => hook()); + return () => remove(element); +} \ No newline at end of file