From 191983dc8e089ce116c99f498c9f2038c288cb2a Mon Sep 17 00:00:00 2001 From: Natxo <1172351+natxocc@users.noreply.github.com> Date: Mon, 16 Mar 2026 00:45:53 +0100 Subject: [PATCH] Enhance flushEffectQueue and storage handling --- src/sigpro.js | 88 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 64 insertions(+), 24 deletions(-) diff --git a/src/sigpro.js b/src/sigpro.js index 03fd53d..eb96403 100644 --- a/src/sigpro.js +++ b/src/sigpro.js @@ -9,15 +9,28 @@ let isFlushScheduled = false; * Flushes all pending effects in the queue * Executes all queued jobs and clears the queue */ +let flushCount = 0; + const flushEffectQueue = () => { isFlushScheduled = false; - try { - for (const effect of effectQueue) { - effect.run(); - } + flushCount++; + + if (flushCount > 100) { effectQueue.clear(); + flushCount = 0; + throw new Error("SigPro: Infinite reactive loop detected."); + } + + try { + const effects = Array.from(effectQueue); + effectQueue.clear(); + for (const effect of effects) effect.run(); } catch (error) { console.error("SigPro Flush Error:", error); + } finally { + setTimeout(() => { + flushCount = 0; + }, 0); } }; @@ -143,17 +156,32 @@ export const $effect = (effectFn) => { * @returns {Function} Signal that persists to storage */ export const $storage = (key, initialValue, storage = localStorage) => { - // Load saved value - const saved = storage.getItem(key); - const signal = $(saved !== null ? JSON.parse(saved) : initialValue); - - // Auto-save on changes - $effect(() => { - const value = signal(); - if (value === undefined || value === null) { - storage.removeItem(key); + let initial; + try { + const saved = storage.getItem(key); + if (saved !== null) { + initial = JSON.parse(saved); } else { - storage.setItem(key, JSON.stringify(value)); + initial = initialValue; + } + } catch (e) { + console.warn(`Error reading ${key} from storage:`, e); + initial = initialValue; + storage.removeItem(key); + } + + const signal = $(initial); + + $effect(() => { + try { + const value = signal(); + if (value === undefined || value === null) { + storage.removeItem(key); + } else { + storage.setItem(key, JSON.stringify(value)); + } + } catch (e) { + console.warn(`Error saving ${key} to storage:`, e); } }); @@ -165,6 +193,7 @@ export const $storage = (key, initialValue, storage = localStorage) => { * @param {string[]} strings - Template strings * @param {...any} values - Dynamic values * @returns {DocumentFragment} Reactive document fragment + * @see {@link https://developer.mozilla.org/es/docs/Glossary/Cross-site_scripting} */ export const html = (strings, ...values) => { const templateCache = html._templateCache ?? (html._templateCache = new WeakMap()); @@ -204,11 +233,13 @@ export const html = (strings, ...values) => { if (typeof result !== "object" && !Array.isArray(result)) { const textNode = startMarker.nextSibling; + const safeText = String(result ?? ""); + if (textNode !== endMarker && textNode?.nodeType === 3) { - textNode.textContent = result ?? ""; + textNode.textContent = safeText; } else { while (startMarker.nextSibling !== endMarker) parent.removeChild(startMarker.nextSibling); - parent.insertBefore(document.createTextNode(result ?? ""), endMarker); + parent.insertBefore(document.createTextNode(safeText), endMarker); } return; } @@ -473,7 +504,14 @@ export const $fetch = async (url, data, loading) => { headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); - return await res.json(); + + const text = await res.text(); + try { + return JSON.parse(text); + } catch (e) { + console.warn("Invalid JSON response"); + return null; + } } catch (e) { return null; } finally { @@ -481,6 +519,11 @@ export const $fetch = async (url, data, loading) => { } }; +const sanitizePath = (path) => { + // Eliminar cualquier intento de HTML/JavaScript + return String(path).replace(/[<>"']/g, ""); +}; + /** * Creates a router for hash-based navigation * @param {Array<{path: string|RegExp, component: Function}>} routes - Route configurations @@ -498,7 +541,7 @@ export const $router = (routes) => { container.style.display = "contents"; window.addEventListener("hashchange", () => { - const nextPath = getCurrentPath(); + const nextPath = sanitizePath(getCurrentPath()); if (currentPath() !== nextPath) currentPath(nextPath); }); @@ -525,11 +568,7 @@ export const $router = (routes) => { activeEffect = null; try { - const view = matchedRoute - ? matchedRoute.component(routeParams) - : html` -