Enhance flushEffectQueue and storage handling

This commit is contained in:
Natxo
2026-03-16 00:45:53 +01:00
committed by GitHub
parent cf322ce435
commit 191983dc8e

View File

@@ -9,15 +9,28 @@ let isFlushScheduled = false;
* Flushes all pending effects in the queue * Flushes all pending effects in the queue
* Executes all queued jobs and clears the queue * Executes all queued jobs and clears the queue
*/ */
let flushCount = 0;
const flushEffectQueue = () => { const flushEffectQueue = () => {
isFlushScheduled = false; isFlushScheduled = false;
try { flushCount++;
for (const effect of effectQueue) {
effect.run(); if (flushCount > 100) {
}
effectQueue.clear(); 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) { } catch (error) {
console.error("SigPro Flush Error:", 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 * @returns {Function} Signal that persists to storage
*/ */
export const $storage = (key, initialValue, storage = localStorage) => { export const $storage = (key, initialValue, storage = localStorage) => {
// Load saved value let initial;
const saved = storage.getItem(key); try {
const signal = $(saved !== null ? JSON.parse(saved) : initialValue); const saved = storage.getItem(key);
if (saved !== null) {
// Auto-save on changes initial = JSON.parse(saved);
$effect(() => {
const value = signal();
if (value === undefined || value === null) {
storage.removeItem(key);
} else { } 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 {string[]} strings - Template strings
* @param {...any} values - Dynamic values * @param {...any} values - Dynamic values
* @returns {DocumentFragment} Reactive document fragment * @returns {DocumentFragment} Reactive document fragment
* @see {@link https://developer.mozilla.org/es/docs/Glossary/Cross-site_scripting}
*/ */
export const html = (strings, ...values) => { export const html = (strings, ...values) => {
const templateCache = html._templateCache ?? (html._templateCache = new WeakMap()); const templateCache = html._templateCache ?? (html._templateCache = new WeakMap());
@@ -204,11 +233,13 @@ export const html = (strings, ...values) => {
if (typeof result !== "object" && !Array.isArray(result)) { if (typeof result !== "object" && !Array.isArray(result)) {
const textNode = startMarker.nextSibling; const textNode = startMarker.nextSibling;
const safeText = String(result ?? "");
if (textNode !== endMarker && textNode?.nodeType === 3) { if (textNode !== endMarker && textNode?.nodeType === 3) {
textNode.textContent = result ?? ""; textNode.textContent = safeText;
} else { } else {
while (startMarker.nextSibling !== endMarker) parent.removeChild(startMarker.nextSibling); while (startMarker.nextSibling !== endMarker) parent.removeChild(startMarker.nextSibling);
parent.insertBefore(document.createTextNode(result ?? ""), endMarker); parent.insertBefore(document.createTextNode(safeText), endMarker);
} }
return; return;
} }
@@ -473,7 +504,14 @@ export const $fetch = async (url, data, loading) => {
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(data), 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) { } catch (e) {
return null; return null;
} finally { } 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 * Creates a router for hash-based navigation
* @param {Array<{path: string|RegExp, component: Function}>} routes - Route configurations * @param {Array<{path: string|RegExp, component: Function}>} routes - Route configurations
@@ -498,7 +541,7 @@ export const $router = (routes) => {
container.style.display = "contents"; container.style.display = "contents";
window.addEventListener("hashchange", () => { window.addEventListener("hashchange", () => {
const nextPath = getCurrentPath(); const nextPath = sanitizePath(getCurrentPath());
if (currentPath() !== nextPath) currentPath(nextPath); if (currentPath() !== nextPath) currentPath(nextPath);
}); });
@@ -525,11 +568,7 @@ export const $router = (routes) => {
activeEffect = null; activeEffect = null;
try { try {
const view = matchedRoute const view = matchedRoute ? matchedRoute.component(routeParams) : Object.assign(document.createElement("h1"), { textContent: "404" });
? matchedRoute.component(routeParams)
: html`
<h1>404</h1>
`;
container.replaceChildren(view instanceof Node ? view : document.createTextNode(view ?? "")); container.replaceChildren(view instanceof Node ? view : document.createTextNode(view ?? ""));
} finally { } finally {
@@ -577,7 +616,8 @@ export const $ws = (url, options = {}) => {
}; };
ws.onmessage = (e) => { ws.onmessage = (e) => {
messages([...messages(), e.data]); const data = e.data;
messages([...messages(), data]);
}; };
ws.onerror = (err) => { ws.onerror = (err) => {