slicejs-web-framework 1.0.26 → 1.0.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,110 @@
1
+ // Slice/Components/Structural/Router/EventThrottler.js
2
+
3
+ /**
4
+ * Sistema de throttling optimizado para eventos del router
5
+ * Mejora significativa sobre el setTimeout básico actual
6
+ */
7
+ export default class EventThrottler {
8
+ constructor() {
9
+ this.timeouts = new Map();
10
+ this.executingCallbacks = new Set();
11
+ this.defaultDelay = 10;
12
+ }
13
+
14
+ /**
15
+ * Throttle con memoria de callbacks en ejecución
16
+ * Evita race conditions y callbacks duplicados
17
+ */
18
+ throttle(key, callback, delay = this.defaultDelay) {
19
+ // Si ya se está ejecutando este callback, ignorar
20
+ if (this.executingCallbacks.has(key)) {
21
+ return Promise.resolve();
22
+ }
23
+
24
+ // Cancelar timeout anterior si existe
25
+ if (this.timeouts.has(key)) {
26
+ clearTimeout(this.timeouts.get(key));
27
+ }
28
+
29
+ return new Promise((resolve, reject) => {
30
+ const timeoutId = setTimeout(async () => {
31
+ this.executingCallbacks.add(key);
32
+ this.timeouts.delete(key);
33
+
34
+ try {
35
+ const result = await callback();
36
+ resolve(result);
37
+ } catch (error) {
38
+ reject(error);
39
+ } finally {
40
+ this.executingCallbacks.delete(key);
41
+ }
42
+ }, delay);
43
+
44
+ this.timeouts.set(key, timeoutId);
45
+ });
46
+ }
47
+
48
+ /**
49
+ * Debounce mejorado con cancelación manual
50
+ */
51
+ debounce(key, callback, delay = this.defaultDelay) {
52
+ if (this.timeouts.has(key)) {
53
+ clearTimeout(this.timeouts.get(key));
54
+ }
55
+
56
+ return new Promise((resolve) => {
57
+ const timeoutId = setTimeout(async () => {
58
+ this.timeouts.delete(key);
59
+ const result = await callback();
60
+ resolve(result);
61
+ }, delay);
62
+
63
+ this.timeouts.set(key, timeoutId);
64
+ });
65
+ }
66
+
67
+ /**
68
+ * Cancelar un throttle/debounce específico
69
+ */
70
+ cancel(key) {
71
+ if (this.timeouts.has(key)) {
72
+ clearTimeout(this.timeouts.get(key));
73
+ this.timeouts.delete(key);
74
+ return true;
75
+ }
76
+ return false;
77
+ }
78
+
79
+ /**
80
+ * Cancelar todos los throttles/debounces pendientes
81
+ */
82
+ cancelAll() {
83
+ for (const timeoutId of this.timeouts.values()) {
84
+ clearTimeout(timeoutId);
85
+ }
86
+ this.timeouts.clear();
87
+ this.executingCallbacks.clear();
88
+ }
89
+
90
+ /**
91
+ * Verificar si hay un throttle/debounce pendiente
92
+ */
93
+ isPending(key) {
94
+ return this.timeouts.has(key);
95
+ }
96
+
97
+ /**
98
+ * Verificar si un callback se está ejecutando
99
+ */
100
+ isExecuting(key) {
101
+ return this.executingCallbacks.has(key);
102
+ }
103
+
104
+ /**
105
+ * Cleanup para evitar memory leaks
106
+ */
107
+ destroy() {
108
+ this.cancelAll();
109
+ }
110
+ }
@@ -0,0 +1,245 @@
1
+ // Slice/Components/Structural/Router/RouteCache.js
2
+
3
+ /**
4
+ * Sistema de caché optimizado para contenedores de rutas
5
+ * Caché individual por contenedor con timestamps y cleanup automático
6
+ */
7
+ export default class RouteCache {
8
+ constructor() {
9
+ this.containerCaches = new Map();
10
+ this.CACHE_DURATION = 100; // ms
11
+ this.processedContainers = new WeakMap();
12
+ this.cleanupInterval = null;
13
+
14
+ // Auto-cleanup cada 5 segundos
15
+ this.startCleanupScheduler();
16
+ }
17
+
18
+ /**
19
+ * Obtener contenedores de rutas con caché inteligente
20
+ */
21
+ getCachedRouteContainers(container) {
22
+ const containerKey = this.getContainerKey(container);
23
+ const now = Date.now();
24
+
25
+ // Verificar caché individual por contenedor
26
+ if (this.containerCaches.has(containerKey)) {
27
+ const cached = this.containerCaches.get(containerKey);
28
+
29
+ // Verificar si el caché es válido y el contenedor sigue conectado
30
+ if ((now - cached.timestamp) < this.CACHE_DURATION &&
31
+ this.isContainerValid(container, cached.containers)) {
32
+ return cached.containers;
33
+ }
34
+ }
35
+
36
+ // Regenerar caché para este contenedor específico
37
+ const routeContainers = this.findAllRouteContainersOptimized(container);
38
+ this.containerCaches.set(containerKey, {
39
+ containers: routeContainers,
40
+ timestamp: now,
41
+ containerRef: new WeakRef(container)
42
+ });
43
+
44
+ return routeContainers;
45
+ }
46
+
47
+ /**
48
+ * Búsqueda optimizada con WeakMap para evitar recorridos repetidos
49
+ */
50
+ findAllRouteContainersOptimized(container) {
51
+ // Si ya procesamos este contenedor recientemente, usar WeakMap
52
+ if (this.processedContainers.has(container)) {
53
+ const cached = this.processedContainers.get(container);
54
+ if (Date.now() - cached.timestamp < 50) { // Cache ultra corto para WeakMap
55
+ return cached.containers.filter(c => c.isConnected);
56
+ }
57
+ }
58
+
59
+ const routeContainers = [];
60
+
61
+ // TreeWalker optimizado con filtro más específico
62
+ const walker = document.createTreeWalker(
63
+ container,
64
+ NodeFilter.SHOW_ELEMENT,
65
+ {
66
+ acceptNode: (node) => {
67
+ // Filtro más eficiente
68
+ const tagName = node.tagName;
69
+ if (tagName === 'SLICE-ROUTE' || tagName === 'SLICE-MULTI-ROUTE') {
70
+ // Verificar que esté conectado y visible
71
+ if (node.isConnected && !node.hasAttribute('disabled')) {
72
+ return NodeFilter.FILTER_ACCEPT;
73
+ }
74
+ }
75
+ return NodeFilter.FILTER_SKIP;
76
+ }
77
+ }
78
+ );
79
+
80
+ let node;
81
+ while (node = walker.nextNode()) {
82
+ routeContainers.push(node);
83
+ }
84
+
85
+ // Guardar en WeakMap para acceso ultra-rápido
86
+ this.processedContainers.set(container, {
87
+ containers: routeContainers,
88
+ timestamp: Date.now()
89
+ });
90
+
91
+ return routeContainers;
92
+ }
93
+
94
+ /**
95
+ * Generar clave única para contenedor
96
+ */
97
+ getContainerKey(container) {
98
+ if (container === document) {
99
+ return 'document';
100
+ }
101
+
102
+ // Usar sliceId si existe, si no, crear una clave basada en posición
103
+ return container.sliceId ||
104
+ container.id ||
105
+ `${container.tagName}-${this.getElementIndex(container)}`;
106
+ }
107
+
108
+ /**
109
+ * Obtener índice del elemento para identificación única
110
+ */
111
+ getElementIndex(element) {
112
+ if (!element.parentNode) return 0;
113
+
114
+ const siblings = Array.from(element.parentNode.children);
115
+ return siblings.indexOf(element);
116
+ }
117
+
118
+ /**
119
+ * Verificar si los contenedores en caché siguen siendo válidos
120
+ */
121
+ isContainerValid(container, cachedContainers) {
122
+ // Verificar que el contenedor principal siga conectado
123
+ if (!container.isConnected) {
124
+ return false;
125
+ }
126
+
127
+ // Verificar que al menos el 80% de los contenedores cached siguen conectados
128
+ const connectedCount = cachedContainers.filter(c => c.isConnected).length;
129
+ const validityThreshold = Math.max(1, Math.floor(cachedContainers.length * 0.8));
130
+
131
+ return connectedCount >= validityThreshold;
132
+ }
133
+
134
+ /**
135
+ * Invalidar caché específico de un contenedor
136
+ */
137
+ invalidateContainer(container) {
138
+ const containerKey = this.getContainerKey(container);
139
+ this.containerCaches.delete(containerKey);
140
+
141
+ if (this.processedContainers.has(container)) {
142
+ this.processedContainers.delete(container);
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Invalidar todo el caché
148
+ */
149
+ invalidateAll() {
150
+ this.containerCaches.clear();
151
+ // No podemos limpiar WeakMap directamente, pero se limpiará automáticamente
152
+ }
153
+
154
+ /**
155
+ * Invalidar caché selectivo basado en cambios DOM
156
+ */
157
+ invalidateByMutation(mutations) {
158
+ const affectedContainers = new Set();
159
+
160
+ mutations.forEach((mutation) => {
161
+ if (mutation.type === 'childList') {
162
+ const addedNodes = Array.from(mutation.addedNodes);
163
+ const removedNodes = Array.from(mutation.removedNodes);
164
+
165
+ // Buscar contenedores afectados
166
+ [...addedNodes, ...removedNodes].forEach(node => {
167
+ if (node.nodeType === Node.ELEMENT_NODE) {
168
+ if (node.tagName === 'SLICE-ROUTE' || node.tagName === 'SLICE-MULTI-ROUTE') {
169
+ affectedContainers.add(mutation.target);
170
+ }
171
+
172
+ // Buscar contenedores padre que podrían estar afectados
173
+ let parent = mutation.target;
174
+ while (parent && parent !== document) {
175
+ if (parent.tagName && (
176
+ parent.tagName.includes('SLICE') ||
177
+ parent.hasAttribute('slice-component'))) {
178
+ affectedContainers.add(parent);
179
+ }
180
+ parent = parent.parentElement;
181
+ }
182
+ }
183
+ });
184
+ }
185
+ });
186
+
187
+ // Invalidar solo los contenedores afectados
188
+ affectedContainers.forEach(container => {
189
+ this.invalidateContainer(container);
190
+ });
191
+ }
192
+
193
+ /**
194
+ * Cleanup automático de cachés obsoletos
195
+ */
196
+ startCleanupScheduler() {
197
+ this.cleanupInterval = setInterval(() => {
198
+ this.cleanupStaleCache();
199
+ }, 5000); // Cada 5 segundos
200
+ }
201
+
202
+ /**
203
+ * Limpiar caché obsoleto
204
+ */
205
+ cleanupStaleCache() {
206
+ const now = Date.now();
207
+ const maxAge = this.CACHE_DURATION * 10; // 10x la duración normal para cleanup
208
+
209
+ for (const [key, cached] of this.containerCaches.entries()) {
210
+ // Eliminar cachés muy antiguos
211
+ if ((now - cached.timestamp) > maxAge) {
212
+ this.containerCaches.delete(key);
213
+ continue;
214
+ }
215
+
216
+ // Eliminar cachés de contenedores que ya no existen
217
+ if (cached.containerRef && !cached.containerRef.deref()) {
218
+ this.containerCaches.delete(key);
219
+ }
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Obtener estadísticas del caché para debugging
225
+ */
226
+ getStats() {
227
+ return {
228
+ totalCaches: this.containerCaches.size,
229
+ oldestCache: Math.min(...Array.from(this.containerCaches.values()).map(c => c.timestamp)),
230
+ newestCache: Math.max(...Array.from(this.containerCaches.values()).map(c => c.timestamp))
231
+ };
232
+ }
233
+
234
+ /**
235
+ * Destruir el caché y cleanup
236
+ */
237
+ destroy() {
238
+ if (this.cleanupInterval) {
239
+ clearInterval(this.cleanupInterval);
240
+ this.cleanupInterval = null;
241
+ }
242
+
243
+ this.invalidateAll();
244
+ }
245
+ }
@@ -0,0 +1,236 @@
1
+ // Slice/Components/Structural/Router/RouteMatcher.js
2
+
3
+ /**
4
+ * Sistema de matching de rutas optimizado
5
+ * Separa la lógica de matching del Router principal
6
+ */
7
+ export default class RouteMatcher {
8
+ constructor(routes) {
9
+ this.routes = routes;
10
+ this.pathToRouteMap = new Map();
11
+ this.compiledPatterns = new Map();
12
+ this.staticRoutes = new Map();
13
+ this.dynamicRoutes = new Map();
14
+
15
+ this.buildOptimizedMaps();
16
+ }
17
+
18
+ /**
19
+ * Construir mapas optimizados para matching rápido
20
+ */
21
+ buildOptimizedMaps() {
22
+ this.pathToRouteMap = this.createPathToRouteMap(this.routes);
23
+
24
+ // Separar rutas estáticas de dinámicas para matching más rápido
25
+ for (const [path, route] of this.pathToRouteMap.entries()) {
26
+ if (path.includes('${')) {
27
+ // Ruta dinámica - pre-compilar el patrón
28
+ const compiled = this.compilePathPattern(path);
29
+ this.compiledPatterns.set(path, compiled);
30
+ this.dynamicRoutes.set(path, route);
31
+ } else {
32
+ // Ruta estática - acceso directo
33
+ this.staticRoutes.set(path, route);
34
+ }
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Crear mapa de rutas con optimizaciones
40
+ */
41
+ createPathToRouteMap(routes, basePath = '', parentRoute = null) {
42
+ const pathToRouteMap = new Map();
43
+
44
+ for (const route of routes) {
45
+ const fullPath = `${basePath}${route.path}`.replace(/\/+/g, '/');
46
+
47
+ const routeWithParent = {
48
+ ...route,
49
+ fullPath,
50
+ parentPath: parentRoute ? parentRoute.fullPath : null,
51
+ parentRoute: parentRoute
52
+ };
53
+
54
+ pathToRouteMap.set(fullPath, routeWithParent);
55
+
56
+ if (route.children) {
57
+ const childPathToRouteMap = this.createPathToRouteMap(
58
+ route.children,
59
+ fullPath,
60
+ routeWithParent
61
+ );
62
+
63
+ for (const [childPath, childRoute] of childPathToRouteMap.entries()) {
64
+ pathToRouteMap.set(childPath, childRoute);
65
+ }
66
+ }
67
+ }
68
+
69
+ return pathToRouteMap;
70
+ }
71
+
72
+ /**
73
+ * Matching optimizado con separación de rutas estáticas/dinámicas
74
+ */
75
+ matchRoute(path) {
76
+ // 1. Buscar primero en rutas estáticas (más rápido)
77
+ const staticMatch = this.staticRoutes.get(path);
78
+ if (staticMatch) {
79
+ return this.formatMatchResult(staticMatch, {});
80
+ }
81
+
82
+ // 2. Buscar en rutas dinámicas solo si no hay match estático
83
+ for (const [routePattern, route] of this.dynamicRoutes.entries()) {
84
+ const compiled = this.compiledPatterns.get(routePattern);
85
+ const match = path.match(compiled.regex);
86
+
87
+ if (match) {
88
+ const params = {};
89
+ compiled.paramNames.forEach((name, i) => {
90
+ params[name] = match[i + 1];
91
+ });
92
+
93
+ return this.formatMatchResult(route, params);
94
+ }
95
+ }
96
+
97
+ // 3. Ruta 404 si no hay matches
98
+ const notFoundRoute = this.staticRoutes.get('/404');
99
+ return this.formatMatchResult(notFoundRoute, {});
100
+ }
101
+
102
+ /**
103
+ * Formatear resultado del match consistentemente
104
+ */
105
+ formatMatchResult(route, params) {
106
+ if (!route) {
107
+ return { route: null, params: {} };
108
+ }
109
+
110
+ if (route.parentRoute) {
111
+ return {
112
+ route: route.parentRoute,
113
+ params: params,
114
+ childRoute: route
115
+ };
116
+ }
117
+
118
+ return { route, params };
119
+ }
120
+
121
+ /**
122
+ * Compilar patrón de ruta con caché
123
+ */
124
+ compilePathPattern(pattern) {
125
+ // Verificar si ya está compilado
126
+ if (this.compiledPatterns.has(pattern)) {
127
+ return this.compiledPatterns.get(pattern);
128
+ }
129
+
130
+ const paramNames = [];
131
+ const regexPattern = '^' + pattern.replace(/\$\{([^}]+)\}/g, (_, paramName) => {
132
+ paramNames.push(paramName);
133
+ return '([^/]+)';
134
+ }) + '$';
135
+
136
+ const compiled = {
137
+ regex: new RegExp(regexPattern),
138
+ paramNames
139
+ };
140
+
141
+ this.compiledPatterns.set(pattern, compiled);
142
+ return compiled;
143
+ }
144
+
145
+ /**
146
+ * Obtener todas las rutas disponibles
147
+ */
148
+ getAllRoutes() {
149
+ return Array.from(this.pathToRouteMap.values());
150
+ }
151
+
152
+ /**
153
+ * Obtener rutas por componente
154
+ */
155
+ getRoutesByComponent(componentName) {
156
+ return Array.from(this.pathToRouteMap.values())
157
+ .filter(route => route.component === componentName);
158
+ }
159
+
160
+ /**
161
+ * Verificar si una ruta existe
162
+ */
163
+ hasRoute(path) {
164
+ return this.staticRoutes.has(path) ||
165
+ Array.from(this.dynamicRoutes.keys()).some(pattern => {
166
+ const compiled = this.compiledPatterns.get(pattern);
167
+ return compiled.regex.test(path);
168
+ });
169
+ }
170
+
171
+ /**
172
+ * Generar URL con parámetros
173
+ */
174
+ generateUrl(routePath, params = {}) {
175
+ if (!routePath.includes('${')) {
176
+ return routePath;
177
+ }
178
+
179
+ return routePath.replace(/\$\{([^}]+)\}/g, (_, paramName) => {
180
+ return params[paramName] || paramName;
181
+ });
182
+ }
183
+
184
+ /**
185
+ * Actualizar rutas dinámicamente
186
+ */
187
+ updateRoutes(newRoutes) {
188
+ this.routes = newRoutes;
189
+ this.pathToRouteMap.clear();
190
+ this.compiledPatterns.clear();
191
+ this.staticRoutes.clear();
192
+ this.dynamicRoutes.clear();
193
+
194
+ this.buildOptimizedMaps();
195
+ }
196
+
197
+ /**
198
+ * Añadir ruta individual
199
+ */
200
+ addRoute(route, basePath = '') {
201
+ const fullPath = `${basePath}${route.path}`.replace(/\/+/g, '/');
202
+ const routeWithPath = { ...route, fullPath };
203
+
204
+ this.pathToRouteMap.set(fullPath, routeWithPath);
205
+
206
+ if (fullPath.includes('${')) {
207
+ const compiled = this.compilePathPattern(fullPath);
208
+ this.compiledPatterns.set(fullPath, compiled);
209
+ this.dynamicRoutes.set(fullPath, routeWithPath);
210
+ } else {
211
+ this.staticRoutes.set(fullPath, routeWithPath);
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Remover ruta
217
+ */
218
+ removeRoute(path) {
219
+ this.pathToRouteMap.delete(path);
220
+ this.staticRoutes.delete(path);
221
+ this.dynamicRoutes.delete(path);
222
+ this.compiledPatterns.delete(path);
223
+ }
224
+
225
+ /**
226
+ * Obtener estadísticas del matcher
227
+ */
228
+ getStats() {
229
+ return {
230
+ totalRoutes: this.pathToRouteMap.size,
231
+ staticRoutes: this.staticRoutes.size,
232
+ dynamicRoutes: this.dynamicRoutes.size,
233
+ compiledPatterns: this.compiledPatterns.size
234
+ };
235
+ }
236
+ }