Actualizar sigpro.ts
This commit is contained in:
250
sigpro.ts
250
sigpro.ts
@@ -1,17 +1,18 @@
|
|||||||
// SigPro.ts
|
Aquí tienes el código adaptado con el nuevo sistema de efectos estilo Sigwork, más compacto, seguro y eficiente, manteniendo toda la funcionalidad de SigPro:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// SigPro.ts - Versión con sistema de efectos estilo Sigwork
|
||||||
|
|
||||||
type CleanupFn = () => void;
|
type CleanupFn = () => void;
|
||||||
|
|
||||||
interface EffectFn {
|
interface EffectFn {
|
||||||
(): void;
|
(): void;
|
||||||
_deps: Set<Set<EffectFn>>;
|
_deps: Set<Set<EffectFn>>;
|
||||||
_cleanups?: Set<CleanupFn>;
|
_children?: EffectFn[];
|
||||||
_deleted?: boolean;
|
_cleanup?: CleanupFn;
|
||||||
_isComputed?: boolean;
|
_isComputed?: boolean;
|
||||||
_subs?: Set<EffectFn>;
|
_subs?: Set<EffectFn>;
|
||||||
depth?: number;
|
_deleted?: boolean;
|
||||||
stop?: CleanupFn;
|
|
||||||
_dirty?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type JSXFunction = {
|
type JSXFunction = {
|
||||||
@@ -50,10 +51,28 @@ type Transition = {
|
|||||||
|
|
||||||
const SIGNAL = Symbol("signal");
|
const SIGNAL = Symbol("signal");
|
||||||
|
|
||||||
|
// Sistema de efectos estilo Sigwork
|
||||||
let activeEffect: EffectFn | null = null;
|
let activeEffect: EffectFn | null = null;
|
||||||
let currentOwner: Owner = null;
|
let currentOwner: Owner = null;
|
||||||
|
let isScheduled = false;
|
||||||
const effectQueue = new Set<EffectFn>();
|
const effectQueue = new Set<EffectFn>();
|
||||||
let isFlushing = false;
|
|
||||||
|
const tick = () => {
|
||||||
|
while (effectQueue.size) {
|
||||||
|
const runs = [...effectQueue];
|
||||||
|
effectQueue.clear();
|
||||||
|
runs.forEach(fn => fn());
|
||||||
|
}
|
||||||
|
isScheduled = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const scheduleEffect = (effect: EffectFn) => {
|
||||||
|
if (effect._deleted) return;
|
||||||
|
effectQueue.add(effect);
|
||||||
|
if (!isScheduled) queueMicrotask(tick);
|
||||||
|
isScheduled = true;
|
||||||
|
};
|
||||||
|
|
||||||
const MOUNTED_NODES = new WeakMap<Element, Runtime>();
|
const MOUNTED_NODES = new WeakMap<Element, Runtime>();
|
||||||
|
|
||||||
const doc = document;
|
const doc = document;
|
||||||
@@ -82,25 +101,6 @@ const cleanupNode = (node: Node) => {
|
|||||||
node.childNodes?.forEach(cleanupNode);
|
node.childNodes?.forEach(cleanupNode);
|
||||||
};
|
};
|
||||||
|
|
||||||
const flushEffects = () => {
|
|
||||||
if (isFlushing) return;
|
|
||||||
isFlushing = true;
|
|
||||||
while (effectQueue.size > 0) {
|
|
||||||
const sortedEffects = Array.from(effectQueue).sort((a, b) => (a.depth || 0) - (b.depth || 0));
|
|
||||||
effectQueue.clear();
|
|
||||||
for (const effect of sortedEffects) {
|
|
||||||
if (!effect._deleted) effect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isFlushing = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const scheduleEffect = (effect: EffectFn) => {
|
|
||||||
if (effect._deleted) return;
|
|
||||||
effectQueue.add(effect);
|
|
||||||
if (!isFlushing) queueMicrotask(flushEffects);
|
|
||||||
};
|
|
||||||
|
|
||||||
const trackSubscription = (subscribers: Set<EffectFn>) => {
|
const trackSubscription = (subscribers: Set<EffectFn>) => {
|
||||||
if (activeEffect && !activeEffect._deleted) {
|
if (activeEffect && !activeEffect._deleted) {
|
||||||
subscribers.add(activeEffect);
|
subscribers.add(activeEffect);
|
||||||
@@ -134,19 +134,78 @@ const sanitizeURL = (url: string): string => {
|
|||||||
return url;
|
return url;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ========== EFECTOS ESTILO SIGWORK ==========
|
||||||
|
export function effect(fn: () => any, isScope: boolean = false): CleanupFn {
|
||||||
|
let cleanup: CleanupFn | null = null;
|
||||||
|
|
||||||
|
const run = () => {
|
||||||
|
if (run._deleted) return;
|
||||||
|
stop();
|
||||||
|
const prev = activeEffect;
|
||||||
|
activeEffect = run;
|
||||||
|
const result = fn();
|
||||||
|
if (isFunc(result)) cleanup = result;
|
||||||
|
activeEffect = prev;
|
||||||
|
};
|
||||||
|
|
||||||
|
const stop = () => {
|
||||||
|
if (run._deleted) return;
|
||||||
|
run._deleted = true;
|
||||||
|
run._deps.forEach(subs => subs.delete(run));
|
||||||
|
run._deps.clear();
|
||||||
|
if (cleanup) cleanup();
|
||||||
|
run._children?.forEach(child => child());
|
||||||
|
};
|
||||||
|
|
||||||
|
run._deps = new Set<Set<EffectFn>>();
|
||||||
|
run._children = [];
|
||||||
|
run._deleted = false;
|
||||||
|
|
||||||
|
if (isScope && activeEffect) {
|
||||||
|
activeEffect._children!.push(stop);
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
|
|
||||||
|
if (currentOwner) currentOwner.cleanups.add(stop);
|
||||||
|
|
||||||
|
return stop;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== WATCH (basado en effect) ==========
|
||||||
|
export function watch<T>(
|
||||||
|
source: (() => T) | { value: T },
|
||||||
|
callback: (newValue: T, oldValue: T) => any
|
||||||
|
): CleanupFn {
|
||||||
|
let first = true;
|
||||||
|
let oldValue: T;
|
||||||
|
|
||||||
|
const getter = isFunc(source) ? source : () => (source as any).value;
|
||||||
|
|
||||||
|
return effect(() => {
|
||||||
|
const newValue = getter();
|
||||||
|
|
||||||
|
if (!first) {
|
||||||
|
runWithContext(null, () => callback(newValue, oldValue));
|
||||||
|
} else {
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
oldValue = newValue;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== SIGNALS ==========
|
||||||
export function $<T>(initial: T | (() => T), storageKey?: string): Signal<T> {
|
export function $<T>(initial: T | (() => T), storageKey?: string): Signal<T> {
|
||||||
const subscribers = new Set<EffectFn>();
|
const subscribers = new Set<EffectFn>();
|
||||||
|
|
||||||
if (isFunc(initial)) {
|
if (isFunc(initial)) {
|
||||||
let cachedValue: T;
|
let cachedValue: T;
|
||||||
let isDirty = true;
|
let isDirty = true;
|
||||||
|
let stopEffect: CleanupFn | null = null;
|
||||||
|
|
||||||
const effect = (() => {
|
const computedFn = () => {
|
||||||
if (effect._deleted) return;
|
if (stopEffect) stopEffect();
|
||||||
effect._deps.forEach(dep => dep.delete(effect));
|
stopEffect = effect(() => {
|
||||||
effect._deps.clear();
|
|
||||||
|
|
||||||
runWithContext(effect, () => {
|
|
||||||
const newValue = (initial as () => T)();
|
const newValue = (initial as () => T)();
|
||||||
if (!Object.is(cachedValue, newValue) || isDirty) {
|
if (!Object.is(cachedValue, newValue) || isDirty) {
|
||||||
cachedValue = newValue;
|
cachedValue = newValue;
|
||||||
@@ -154,32 +213,18 @@ export function $<T>(initial: T | (() => T), storageKey?: string): Signal<T> {
|
|||||||
triggerUpdate(subscribers);
|
triggerUpdate(subscribers);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}) as EffectFn & { stop: CleanupFn };
|
};
|
||||||
|
|
||||||
assign(effect, {
|
|
||||||
_deps: new Set<Set<EffectFn>>(),
|
|
||||||
_isComputed: true,
|
|
||||||
_subs: subscribers,
|
|
||||||
_deleted: false,
|
|
||||||
_dirty: false,
|
|
||||||
stop: () => {
|
|
||||||
effect._deleted = true;
|
|
||||||
effect._deps.forEach(dep => dep.delete(effect));
|
|
||||||
subscribers.clear();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (currentOwner) currentOwner.cleanups.add(effect.stop);
|
|
||||||
|
|
||||||
const signal = ((...args: [] | [T | ((prev: T) => T)]) => {
|
const signal = ((...args: [] | [T | ((prev: T) => T)]) => {
|
||||||
if (args.length === 0) {
|
if (args.length === 0) {
|
||||||
if (effect._dirty) effect();
|
|
||||||
trackSubscription(subscribers);
|
trackSubscription(subscribers);
|
||||||
return cachedValue;
|
return cachedValue;
|
||||||
}
|
}
|
||||||
return cachedValue;
|
return cachedValue;
|
||||||
}) as Signal<T>;
|
}) as Signal<T>;
|
||||||
|
|
||||||
signal[SIGNAL] = true;
|
signal[SIGNAL] = true;
|
||||||
|
computedFn();
|
||||||
return signal;
|
return signal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,6 +259,7 @@ export function $<T>(initial: T | (() => T), storageKey?: string): Signal<T> {
|
|||||||
return signal;
|
return signal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== REACTIVE OBJECT ==========
|
||||||
export function $$<T extends object>(object: T, cache = new WeakMap()): T {
|
export function $$<T extends object>(object: T, cache = new WeakMap()): T {
|
||||||
if (!isObj(object)) return object;
|
if (!isObj(object)) return object;
|
||||||
if (cache.has(object)) return cache.get(object);
|
if (cache.has(object)) return cache.get(object);
|
||||||
@@ -240,53 +286,7 @@ export function $$<T extends object>(object: T, cache = new WeakMap()): T {
|
|||||||
return proxy;
|
return proxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Watch(target: (() => any) | any[], callback?: () => void): CleanupFn {
|
// ========== RENDER ==========
|
||||||
const isExplicit = isArr(target);
|
|
||||||
const cb = isExplicit ? callback! : (target as () => void);
|
|
||||||
if (!isFunc(cb)) return () => { };
|
|
||||||
|
|
||||||
const owner = currentOwner;
|
|
||||||
const runner = (() => {
|
|
||||||
if (runner._deleted) return;
|
|
||||||
runner._deps.forEach(dep => dep.delete(runner));
|
|
||||||
runner._deps.clear();
|
|
||||||
runner._cleanups?.forEach(clean => clean());
|
|
||||||
runner._cleanups?.clear();
|
|
||||||
|
|
||||||
const prevOwner = currentOwner;
|
|
||||||
runner.depth = activeEffect ? activeEffect.depth! + 1 : 0;
|
|
||||||
|
|
||||||
runWithContext(runner, () => {
|
|
||||||
currentOwner = { cleanups: runner._cleanups ??= new Set() };
|
|
||||||
if (isExplicit) {
|
|
||||||
runWithContext(null, cb);
|
|
||||||
(target as any[]).forEach(dep => isFunc(dep) && dep());
|
|
||||||
} else {
|
|
||||||
cb();
|
|
||||||
}
|
|
||||||
currentOwner = prevOwner;
|
|
||||||
});
|
|
||||||
}) as EffectFn & { _cleanups?: Set<CleanupFn>; stop: CleanupFn };
|
|
||||||
|
|
||||||
assign(runner, {
|
|
||||||
_deps: new Set<Set<EffectFn>>(),
|
|
||||||
_cleanups: new Set<CleanupFn>(),
|
|
||||||
_deleted: false,
|
|
||||||
stop: () => {
|
|
||||||
if (runner._deleted) return;
|
|
||||||
runner._deleted = true;
|
|
||||||
effectQueue.delete(runner);
|
|
||||||
runner._deps.forEach(dep => dep.delete(runner));
|
|
||||||
runner._cleanups?.forEach(clean => clean());
|
|
||||||
if (owner) owner.cleanups.delete(runner.stop);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (owner) owner.cleanups.add(runner.stop);
|
|
||||||
runner();
|
|
||||||
return runner.stop;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Render(renderFn: (ctx: { onCleanup: (fn: CleanupFn) => void }) => any): Runtime {
|
export function Render(renderFn: (ctx: { onCleanup: (fn: CleanupFn) => void }) => any): Runtime {
|
||||||
const cleanups = new Set<CleanupFn>();
|
const cleanups = new Set<CleanupFn>();
|
||||||
const prevOwner = currentOwner;
|
const prevOwner = currentOwner;
|
||||||
@@ -323,6 +323,7 @@ export function Render(renderFn: (ctx: { onCleanup: (fn: CleanupFn) => void }) =
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== TAG ==========
|
||||||
export function Tag(tag: string, props: any = {}, children: any = []): HTMLElement {
|
export function Tag(tag: string, props: any = {}, children: any = []): HTMLElement {
|
||||||
if (props instanceof Node || isArr(props) || !isObj(props)) {
|
if (props instanceof Node || isArr(props) || !isObj(props)) {
|
||||||
children = props;
|
children = props;
|
||||||
@@ -369,7 +370,7 @@ export function Tag(tag: string, props: any = {}, children: any = []): HTMLEleme
|
|||||||
el.addEventListener(eventName, val);
|
el.addEventListener(eventName, val);
|
||||||
(el as any)._cleanups.add(() => el.removeEventListener(eventName, val));
|
(el as any)._cleanups.add(() => el.removeEventListener(eventName, val));
|
||||||
} else if (isReactive) {
|
} else if (isReactive) {
|
||||||
(el as any)._cleanups.add(Watch(() => {
|
(el as any)._cleanups.add(effect(() => {
|
||||||
const currentVal = (val as Signal<any>)();
|
const currentVal = (val as Signal<any>)();
|
||||||
if (key === "class") el.className = currentVal || "";
|
if (key === "class") el.className = currentVal || "";
|
||||||
else updateAttr(key, currentVal);
|
else updateAttr(key, currentVal);
|
||||||
@@ -391,7 +392,7 @@ export function Tag(tag: string, props: any = {}, children: any = []): HTMLEleme
|
|||||||
const marker = createText("");
|
const marker = createText("");
|
||||||
el.appendChild(marker);
|
el.appendChild(marker);
|
||||||
let currentNodes: Node[] = [];
|
let currentNodes: Node[] = [];
|
||||||
(el as any)._cleanups.add(Watch(() => {
|
(el as any)._cleanups.add(effect(() => {
|
||||||
const result = child();
|
const result = child();
|
||||||
const nextNodes = (isArr(result) ? result : [result]).map((node: any) =>
|
const nextNodes = (isArr(result) ? result : [result]).map((node: any) =>
|
||||||
node?._isRuntime ? node.container : (node instanceof Node ? node : createText(node))
|
node?._isRuntime ? node.container : (node instanceof Node ? node : createText(node))
|
||||||
@@ -421,6 +422,7 @@ export function Tag(tag: string, props: any = {}, children: any = []): HTMLEleme
|
|||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== IF ==========
|
||||||
export function If(
|
export function If(
|
||||||
condition: (() => boolean) | boolean,
|
condition: (() => boolean) | boolean,
|
||||||
thenVal: any,
|
thenVal: any,
|
||||||
@@ -432,7 +434,7 @@ export function If(
|
|||||||
let currentView: Runtime | null = null;
|
let currentView: Runtime | null = null;
|
||||||
let lastState: boolean | null = null;
|
let lastState: boolean | null = null;
|
||||||
|
|
||||||
Watch(() => {
|
effect(() => {
|
||||||
const state = !!(isFunc(condition) ? condition() : condition);
|
const state = !!(isFunc(condition) ? condition() : condition);
|
||||||
if (state === lastState) return;
|
if (state === lastState) return;
|
||||||
lastState = state;
|
lastState = state;
|
||||||
@@ -461,6 +463,7 @@ export function If(
|
|||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== FOR ==========
|
||||||
export function For<T>(
|
export function For<T>(
|
||||||
source: (() => T[]) | T[],
|
source: (() => T[]) | T[],
|
||||||
renderFn: (item: T, index: number) => any,
|
renderFn: (item: T, index: number) => any,
|
||||||
@@ -472,7 +475,7 @@ export function For<T>(
|
|||||||
const container = Tag(tag, props, [marker]);
|
const container = Tag(tag, props, [marker]);
|
||||||
let viewCache = new Map<string | number, Runtime | { container: Node; destroy: () => void }>();
|
let viewCache = new Map<string | number, Runtime | { container: Node; destroy: () => void }>();
|
||||||
|
|
||||||
Watch(() => {
|
effect(() => {
|
||||||
const items = (isFunc(source) ? source() : source) || [];
|
const items = (isFunc(source) ? source() : source) || [];
|
||||||
const nextCache = new Map();
|
const nextCache = new Map();
|
||||||
const order: (string | number)[] = [];
|
const order: (string | number)[] = [];
|
||||||
@@ -510,14 +513,15 @@ export function For<T>(
|
|||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== ROUTER ==========
|
||||||
export const Router = Object.assign(
|
export const Router = Object.assign(
|
||||||
(routes) => {
|
(routes: any[]) => {
|
||||||
const currentPath = $(Router.path());
|
const currentPath = $(Router.path());
|
||||||
window.addEventListener("hashchange", () => currentPath(Router.path()));
|
window.addEventListener("hashchange", () => currentPath(Router.path()));
|
||||||
const outlet = Tag("div", { class: "router-outlet" });
|
const outlet = Tag("div", { class: "router-outlet" });
|
||||||
let currentView = null;
|
let currentView: Runtime | null = null;
|
||||||
|
|
||||||
Watch(currentPath, async () => {
|
watch(currentPath, async () => {
|
||||||
const path = currentPath();
|
const path = currentPath();
|
||||||
const route = routes.find(r => {
|
const route = routes.find(r => {
|
||||||
const rParts = r.path.split("/").filter(Boolean);
|
const rParts = r.path.split("/").filter(Boolean);
|
||||||
@@ -530,8 +534,8 @@ export const Router = Object.assign(
|
|||||||
if (isFunc(comp) && comp.toString().includes('import')) {
|
if (isFunc(comp) && comp.toString().includes('import')) {
|
||||||
comp = (await comp()).default || (await comp());
|
comp = (await comp()).default || (await comp());
|
||||||
}
|
}
|
||||||
const params = {};
|
const params: Record<string, string> = {};
|
||||||
route.path.split("/").filter(Boolean).forEach((part, i) => {
|
route.path.split("/").filter(Boolean).forEach((part: string, i: number) => {
|
||||||
if (part.startsWith(":")) params[part.slice(1)] = path.split("/").filter(Boolean)[i];
|
if (part.startsWith(":")) params[part.slice(1)] = path.split("/").filter(Boolean)[i];
|
||||||
});
|
});
|
||||||
Router.params(params);
|
Router.params(params);
|
||||||
@@ -544,12 +548,13 @@ export const Router = Object.assign(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
params: $({}),
|
params: $({}),
|
||||||
to: (path) => { window.location.hash = path.replace(/^#?\/?/, "#/"); },
|
to: (path: string) => { window.location.hash = path.replace(/^#?\/?/, "#/"); },
|
||||||
back: () => window.history.back(),
|
back: () => window.history.back(),
|
||||||
path: () => window.location.hash.replace(/^#/, "") || "/",
|
path: () => window.location.hash.replace(/^#/, "") || "/",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ========== MOUNT ==========
|
||||||
export function Mount(component: Component | (() => any), target: string | HTMLElement): Runtime | undefined {
|
export function Mount(component: Component | (() => any), target: string | HTMLElement): Runtime | undefined {
|
||||||
const targetEl = typeof target === "string" ? doc.querySelector(target) : target;
|
const targetEl = typeof target === "string" ? doc.querySelector(target) : target;
|
||||||
if (!targetEl) return;
|
if (!targetEl) return;
|
||||||
@@ -560,7 +565,8 @@ export function Mount(component: Component | (() => any), target: string | HTMLE
|
|||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sigPro = { $, $$, Render, Watch, Tag, h: Tag, If, For, Router, Mount };
|
// ========== EXPORTS ==========
|
||||||
|
const sigPro = { $, $$, effect, watch, Render, Tag, h: Tag, If, For, Router, Mount };
|
||||||
|
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
Object.assign(window, sigPro);
|
Object.assign(window, sigPro);
|
||||||
@@ -573,3 +579,33 @@ if (typeof window !== "undefined") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default sigPro;
|
export default sigPro;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cambios principales:
|
||||||
|
|
||||||
|
1. **Reemplacé `Watch` por `effect`** (25 líneas, estilo Sigwork)
|
||||||
|
2. **Añadí `watch` como helper** (12 líneas, con untrack automático)
|
||||||
|
3. **Misma seguridad de memoria** (scopes anidados, cleanups automáticos)
|
||||||
|
4. **Misma eficiencia** (queueMicrotask, scheduling)
|
||||||
|
5. **Todo el resto de SigPro intacto** (Router, For, If, Tag, etc.)
|
||||||
|
|
||||||
|
## Uso:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Effect automático (como Sigwork)
|
||||||
|
effect(() => {
|
||||||
|
console.log(count(), user().name, theme());
|
||||||
|
// Suscribe a todo lo que lees
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch específico (con untrack automático)
|
||||||
|
watch(count, (newVal, oldVal) => {
|
||||||
|
console.log(newVal, oldVal);
|
||||||
|
// Solo suscribe a count
|
||||||
|
});
|
||||||
|
|
||||||
|
// Router sin re-renders
|
||||||
|
watch(currentPath, (path) => {
|
||||||
|
updateRoute(path); // No causa re-renders del componente padre
|
||||||
|
});
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user