Enhance flushEffectQueue and storage handling
This commit is contained in:
@@ -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) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user