slicejs-web-framework 1.0.33 → 1.0.34

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.
@@ -1,380 +1,305 @@
1
- // Slice/Components/Structural/Router/Router.js
2
-
3
- import EventThrottler from './EventThrottler.js';
4
- import RouteCache from './RouteCache.js';
5
- import RouteMatcher from './RouteMatcher.js';
6
- import RouteRenderer from './RouteRenderer.js';
7
-
8
- /**
9
- * Router optimizado con separación de responsabilidades
10
- * Mejoras significativas en performance y mantenibilidad
11
- */
12
1
  export default class Router {
13
2
  constructor(routes) {
14
3
  this.routes = routes;
15
4
  this.activeRoute = null;
5
+ this.pathToRouteMap = this.createPathToRouteMap(routes);
16
6
 
17
- // Inicializar sistemas especializados
18
- this.eventThrottler = new EventThrottler();
19
- this.routeCache = new RouteCache();
20
- this.routeMatcher = new RouteMatcher(routes);
21
- this.routeRenderer = new RouteRenderer(this.routeCache);
7
+ // NUEVO: Sistema de caché optimizado
8
+ this.routeContainersCache = new Map();
9
+ this.lastCacheUpdate = 0;
10
+ this.CACHE_DURATION = 100; // ms - caché muy corto pero efectivo
22
11
 
23
- // Observer para cambios DOM
24
- this.mutationObserver = null;
25
-
26
- // Estado del router
27
- this.isInitialized = false;
28
- this.isNavigating = false;
12
+ // NUEVO: Observer para invalidar caché automáticamente
13
+ this.setupMutationObserver();
29
14
  }
30
15
 
31
- /**
32
- * Inicializar router con observadores optimizados
33
- */
34
16
  async init() {
35
- if (this.isInitialized) {
36
- slice.logger.logWarning('Router', 'Router already initialized');
37
- return;
38
- }
39
-
40
- try {
41
- // Configurar observador de mutaciones optimizado
42
- this.setupMutationObserver();
43
-
44
- // Cargar ruta inicial
45
- await this.loadInitialRoute();
46
-
47
- // Configurar listeners de navegación
48
- this.setupNavigationListeners();
49
-
50
- this.isInitialized = true;
51
- slice.logger.logInfo('Router', 'Router initialized successfully');
52
-
53
- } catch (error) {
54
- slice.logger.logError('Router', 'Error initializing router', error);
55
- throw error;
56
- }
17
+ await this.loadInitialRoute();
18
+ window.addEventListener('popstate', this.onRouteChange.bind(this));
57
19
  }
58
20
 
59
- /**
60
- * Configurar observador de mutaciones optimizado
61
- */
21
+ // NUEVO: Observer para detectar cambios en el DOM
62
22
  setupMutationObserver() {
63
- if (typeof MutationObserver === 'undefined') {
64
- slice.logger.logWarning('Router', 'MutationObserver not available');
65
- return;
66
- }
67
-
68
- this.mutationObserver = new MutationObserver((mutations) => {
69
- // Usar throttling para evitar múltiples invalidaciones
70
- this.eventThrottler.throttle('cache-invalidation', () => {
71
- this.routeCache.invalidateByMutation(mutations);
72
- }, 50);
73
- });
74
-
75
- this.mutationObserver.observe(document.body, {
76
- childList: true,
77
- subtree: true,
78
- attributeFilter: ['slice-route', 'slice-multi-route']
79
- });
80
- }
81
-
82
- /**
83
- * Configurar listeners de navegación
84
- */
85
- setupNavigationListeners() {
86
- // Listener para popstate (back/forward)
87
- window.addEventListener('popstate', (event) => {
88
- this.eventThrottler.throttle('popstate', () => {
89
- return this.onRouteChange();
23
+ if (typeof MutationObserver !== 'undefined') {
24
+ this.observer = new MutationObserver((mutations) => {
25
+ let shouldInvalidateCache = false;
26
+
27
+ mutations.forEach((mutation) => {
28
+ if (mutation.type === 'childList') {
29
+ // Solo invalidar si se añadieron/removieron nodos que podrían ser rutas
30
+ const addedNodes = Array.from(mutation.addedNodes);
31
+ const removedNodes = Array.from(mutation.removedNodes);
32
+
33
+ const hasRouteNodes = [...addedNodes, ...removedNodes].some(node =>
34
+ node.nodeType === Node.ELEMENT_NODE &&
35
+ (node.tagName === 'SLICE-ROUTE' ||
36
+ node.tagName === 'SLICE-MULTI-ROUTE' ||
37
+ node.querySelector?.('slice-route, slice-multi-route'))
38
+ );
39
+
40
+ if (hasRouteNodes) {
41
+ shouldInvalidateCache = true;
42
+ }
43
+ }
44
+ });
45
+
46
+ if (shouldInvalidateCache) {
47
+ this.invalidateCache();
48
+ }
49
+ });
50
+
51
+ this.observer.observe(document.body, {
52
+ childList: true,
53
+ subtree: true
90
54
  });
91
- });
92
-
93
- // Interceptación automática de enlaces (activada por defecto)
94
- // Para desactivar: agregar disableAutoInterceptLinks: true en la configuración
95
- if (!this.routes.disableAutoInterceptLinks) {
96
- this.setupLinkInterception();
97
- slice.logger.logInfo('Router', 'Auto link interception enabled');
98
55
  }
99
56
  }
100
57
 
101
- /**
102
- * Configurar interceptación de enlaces
103
- * Convierte todos los <a href="/path"> en slice.router.navigate()
104
- */
105
- setupLinkInterception() {
106
- document.addEventListener('click', (event) => {
107
- const link = event.target.closest('a[href]');
108
- if (link && this.shouldInterceptLink(link)) {
109
- event.preventDefault();
110
-
111
- const href = link.getAttribute('href');
112
- slice.logger.logInfo('Router', `Intercepting link: ${href}`);
113
-
114
- this.navigate(href);
115
- }
116
- });
58
+ // NUEVO: Invalidar caché
59
+ invalidateCache() {
60
+ this.routeContainersCache.clear();
61
+ this.lastCacheUpdate = 0;
117
62
  }
118
63
 
119
- /**
120
- * Verificar si debe interceptar el enlace
121
- */
122
- shouldInterceptLink(link) {
123
- const href = link.getAttribute('href');
124
-
125
- // No interceptar si no hay href
126
- if (!href) return false;
127
-
128
- // No interceptar enlaces externos (diferentes dominio)
129
- if (href.startsWith('http://') || href.startsWith('https://')) {
130
- const linkUrl = new URL(href, window.location.origin);
131
- if (linkUrl.origin !== window.location.origin) {
132
- return false;
133
- }
134
- }
135
-
136
- // No interceptar protocolos especiales
137
- if (href.startsWith('mailto:') ||
138
- href.startsWith('tel:') ||
139
- href.startsWith('sms:') ||
140
- href.startsWith('ftp:')) {
141
- return false;
64
+ createPathToRouteMap(routes, basePath = '', parentRoute = null) {
65
+ const pathToRouteMap = new Map();
66
+
67
+ for (const route of routes) {
68
+ const fullPath = `${basePath}${route.path}`.replace(/\/+/g, '/');
69
+
70
+ const routeWithParent = {
71
+ ...route,
72
+ fullPath,
73
+ parentPath: parentRoute ? parentRoute.fullPath : null,
74
+ parentRoute: parentRoute
75
+ };
76
+
77
+ pathToRouteMap.set(fullPath, routeWithParent);
78
+
79
+ if (route.children) {
80
+ const childPathToRouteMap = this.createPathToRouteMap(
81
+ route.children,
82
+ fullPath,
83
+ routeWithParent
84
+ );
85
+
86
+ for (const [childPath, childRoute] of childPathToRouteMap.entries()) {
87
+ pathToRouteMap.set(childPath, childRoute);
88
+ }
89
+ }
142
90
  }
143
-
144
- // No interceptar anchors (#hash)
145
- if (href.startsWith('#')) {
146
- return false;
147
- }
148
-
149
- // No interceptar si tiene atributos especiales
150
- if (link.hasAttribute('download') ||
151
- link.target === '_blank' ||
152
- link.target === '_top' ||
153
- link.target === '_parent' ||
154
- link.hasAttribute('data-no-intercept') ||
155
- link.hasAttribute('data-external')) {
156
- return false;
157
- }
158
-
159
- // No interceptar si está marcado como externo
160
- if (link.classList.contains('external-link') ||
161
- link.classList.contains('no-intercept')) {
162
- return false;
163
- }
164
-
165
- return true;
91
+
92
+ return pathToRouteMap;
166
93
  }
167
94
 
168
- /**
169
- * Manejar cambio de ruta con throttling optimizado
170
- */
171
- async onRouteChange() {
172
- if (this.isNavigating) {
173
- return;
174
- }
95
+ // OPTIMIZADO: Sistema de caché inteligente
96
+ async renderRoutesComponentsInPage(searchContainer = document) {
97
+ let routerContainersFlag = false;
98
+ const routeContainers = this.getCachedRouteContainers(searchContainer);
175
99
 
176
- return this.eventThrottler.throttle('route-change', async () => {
177
- this.isNavigating = true;
178
-
100
+ for (const routeContainer of routeContainers) {
179
101
  try {
180
- const path = window.location.pathname;
181
-
182
- // Intentar renderizar rutas en componentes existentes primero
183
- const routeContainersFlag = await this.routeRenderer.renderRoutesComponentsInPage();
184
-
185
- if (routeContainersFlag) {
186
- return;
102
+ // Verificar que el componente aún esté conectado al DOM
103
+ if (!routeContainer.isConnected) {
104
+ this.invalidateCache();
105
+ continue;
187
106
  }
188
107
 
189
- // Si no hay contenedores de rutas, hacer matching tradicional
190
- const { route, params } = this.routeMatcher.matchRoute(path);
191
- if (route) {
192
- await this.routeRenderer.handleRoute(route, params);
108
+ let response = await routeContainer.renderIfCurrentRoute();
109
+ if (response) {
110
+ this.activeRoute = routeContainer.props;
111
+ routerContainersFlag = true;
193
112
  }
194
-
195
113
  } catch (error) {
196
- slice.logger.logError('Router', 'Error during route change', error);
197
- } finally {
198
- this.isNavigating = false;
114
+ slice.logger.logError('Router', `Error rendering route container`, error);
199
115
  }
200
- }, 10);
201
- }
202
-
203
- /**
204
- * Navegar a una ruta específica
205
- */
206
- async navigate(path, options = {}) {
207
- if (!path || path === window.location.pathname) {
208
- return;
209
116
  }
210
117
 
211
- try {
212
- const { replace = false, state = {} } = options;
213
-
214
- // Actualizar historia del navegador
215
- if (replace) {
216
- window.history.replaceState(state, '', window.location.origin + path);
217
- } else {
218
- window.history.pushState(state, '', window.location.origin + path);
219
- }
220
-
221
- // Ejecutar cambio de ruta
222
- await this.onRouteChange();
223
-
224
- } catch (error) {
225
- slice.logger.logError('Router', `Error navigating to ${path}`, error);
226
- }
118
+ return routerContainersFlag;
227
119
  }
228
120
 
229
- /**
230
- * Navegar hacia atrás
231
- */
232
- back() {
233
- window.history.back();
234
- }
121
+ // NUEVO: Obtener contenedores con caché
122
+ getCachedRouteContainers(container) {
123
+ const containerKey = container === document ? 'document' : container.sliceId || 'anonymous';
124
+ const now = Date.now();
125
+
126
+ // Verificar si el caché es válido
127
+ if (this.routeContainersCache.has(containerKey) &&
128
+ (now - this.lastCacheUpdate) < this.CACHE_DURATION) {
129
+ return this.routeContainersCache.get(containerKey);
130
+ }
235
131
 
236
- /**
237
- * Navegar hacia adelante
238
- */
239
- forward() {
240
- window.history.forward();
132
+ // Regenerar caché
133
+ const routeContainers = this.findAllRouteContainersOptimized(container);
134
+ this.routeContainersCache.set(containerKey, routeContainers);
135
+ this.lastCacheUpdate = now;
136
+
137
+ return routeContainers;
241
138
  }
242
139
 
243
- /**
244
- * Cargar ruta inicial
245
- */
246
- async loadInitialRoute() {
247
- const path = window.location.pathname;
248
- const { route, params } = this.routeMatcher.matchRoute(path);
140
+ // OPTIMIZADO: Búsqueda más eficiente usando TreeWalker
141
+ findAllRouteContainersOptimized(container) {
142
+ const routeContainers = [];
143
+
144
+ // Usar TreeWalker para una búsqueda más eficiente
145
+ const walker = document.createTreeWalker(
146
+ container,
147
+ NodeFilter.SHOW_ELEMENT,
148
+ {
149
+ acceptNode: (node) => {
150
+ // Solo aceptar nodos que sean slice-route o slice-multi-route
151
+ if (node.tagName === 'SLICE-ROUTE' || node.tagName === 'SLICE-MULTI-ROUTE') {
152
+ return NodeFilter.FILTER_ACCEPT;
153
+ }
154
+ return NodeFilter.FILTER_SKIP;
155
+ }
156
+ }
157
+ );
249
158
 
250
- if (route) {
251
- await this.routeRenderer.handleRoute(route, params);
252
- } else {
253
- slice.logger.logWarning('Router', `No route found for initial path: ${path}`);
159
+ let node;
160
+ while (node = walker.nextNode()) {
161
+ routeContainers.push(node);
254
162
  }
255
- }
256
163
 
257
- /**
258
- * Métodos de conveniencia para acceso a subsistemas
259
- */
260
-
261
- // Acceso al matcher
262
- matchRoute(path) {
263
- return this.routeMatcher.matchRoute(path);
164
+ return routeContainers;
264
165
  }
265
166
 
266
- hasRoute(path) {
267
- return this.routeMatcher.hasRoute(path);
268
- }
167
+ // NUEVO: Método específico para renderizar rutas dentro de un componente
168
+ async renderRoutesInComponent(component) {
169
+ if (!component) {
170
+ slice.logger.logWarning('Router', 'No component provided for route rendering');
171
+ return false;
172
+ }
269
173
 
270
- generateUrl(routePath, params) {
271
- return this.routeMatcher.generateUrl(routePath, params);
174
+ return await this.renderRoutesComponentsInPage(component);
272
175
  }
273
176
 
274
- // Acceso al caché
275
- invalidateCache() {
276
- this.routeCache.invalidateAll();
277
- }
177
+ // OPTIMIZADO: Debouncing para evitar múltiples llamadas seguidas
178
+ async onRouteChange() {
179
+ // Cancelar el timeout anterior si existe
180
+ if (this.routeChangeTimeout) {
181
+ clearTimeout(this.routeChangeTimeout);
182
+ }
278
183
 
279
- getCacheStats() {
280
- return this.routeCache.getStats();
281
- }
184
+ // Debounce de 10ms para evitar múltiples llamadas seguidas
185
+ this.routeChangeTimeout = setTimeout(async () => {
186
+ const path = window.location.pathname;
187
+ const routeContainersFlag = await this.renderRoutesComponentsInPage();
282
188
 
283
- // Acceso al renderer
284
- async renderRoutesInComponent(component) {
285
- return this.routeRenderer.renderRoutesInComponent(component);
286
- }
189
+ if (routeContainersFlag) {
190
+ return;
191
+ }
287
192
 
288
- getRendererStats() {
289
- return this.routeRenderer.getStats();
193
+ const { route, params } = this.matchRoute(path);
194
+ if (route) {
195
+ await this.handleRoute(route, params);
196
+ }
197
+ }, 10);
290
198
  }
291
199
 
292
- /**
293
- * Actualizar rutas dinámicamente
294
- */
295
- updateRoutes(newRoutes) {
296
- this.routes = newRoutes;
297
- this.routeMatcher.updateRoutes(newRoutes);
298
- this.invalidateCache();
200
+ async navigate(path) {
201
+ window.history.pushState({}, path, window.location.origin + path);
202
+ await this.onRouteChange();
299
203
  }
300
204
 
301
- /**
302
- * Añadir ruta individual
303
- */
304
- addRoute(route, basePath = '') {
305
- this.routeMatcher.addRoute(route, basePath);
306
- this.invalidateCache();
307
- }
205
+ async handleRoute(route, params) {
206
+ const targetElement = document.querySelector('#app');
207
+
208
+ const componentName = route.parentRoute ? route.parentRoute.component : route.component;
209
+ const sliceId = `route-${componentName}`;
210
+
211
+ const existingComponent = slice.controller.getComponent(sliceId);
308
212
 
309
- /**
310
- * Remover ruta
311
- */
312
- removeRoute(path) {
313
- this.routeMatcher.removeRoute(path);
314
- this.invalidateCache();
213
+ if (slice.loading) {
214
+ slice.loading.start();
315
215
  }
316
216
 
317
- /**
318
- * Obtener todas las rutas
319
- */
320
- getAllRoutes() {
321
- return this.routeMatcher.getAllRoutes();
322
- }
217
+ if (existingComponent) {
218
+ targetElement.innerHTML = '';
219
+ if (existingComponent.update) {
220
+ existingComponent.props = { ...existingComponent.props, ...params };
221
+ await existingComponent.update();
222
+ }
223
+ targetElement.appendChild(existingComponent);
224
+ // Renderizar DESPUÉS de insertar (pero antes de mostrar)
225
+ await this.renderRoutesInComponent(existingComponent);
226
+ } else {
227
+ const component = await slice.build(componentName, {
228
+ params,
229
+ sliceId: sliceId,
230
+ });
323
231
 
324
- /**
325
- * Obtener estadísticas completas del router
326
- */
327
- getStats() {
328
- return {
329
- isInitialized: this.isInitialized,
330
- isNavigating: this.isNavigating,
331
- activeRoute: this.activeRoute,
332
- matcher: this.routeMatcher.getStats(),
333
- cache: this.routeCache.getStats(),
334
- renderer: this.routeRenderer.getStats(),
335
- eventThrottler: {
336
- pendingEvents: this.eventThrottler.timeouts.size
337
- }
338
- };
232
+ targetElement.innerHTML = '';
233
+ targetElement.appendChild(component);
234
+
235
+ // Renderizar INMEDIATAMENTE después de insertar
236
+ await this.renderRoutesInComponent(component);
339
237
  }
340
238
 
341
- /**
342
- * Destruir router y cleanup
343
- */
344
- destroy() {
345
- // Detener observadores
346
- if (this.mutationObserver) {
347
- this.mutationObserver.disconnect();
348
- this.mutationObserver = null;
349
- }
239
+ // Invalidar caché después de cambios importantes en el DOM
240
+ this.invalidateCache();
350
241
 
351
- // Cancelar eventos pendientes
352
- this.eventThrottler.destroy();
242
+ if (slice.loading) {
243
+ slice.loading.stop();
244
+ }
353
245
 
354
- // Limpiar subsistemas
355
- this.routeCache.destroy();
356
- this.routeRenderer.destroy();
246
+ slice.router.activeRoute = route;
247
+ }
357
248
 
358
- // Remover listeners
359
- window.removeEventListener('popstate', this.onRouteChange);
249
+ async loadInitialRoute() {
250
+ const path = window.location.pathname;
251
+ const { route, params } = this.matchRoute(path);
360
252
 
361
- this.isInitialized = false;
362
-
363
- slice.logger.logInfo('Router', 'Router destroyed successfully');
253
+ await this.handleRoute(route, params);
364
254
  }
365
255
 
366
- /**
367
- * Reinicializar router (útil para testing)
368
- */
369
- async reinitialize(newRoutes = null) {
370
- this.destroy();
371
-
372
- if (newRoutes) {
373
- this.routes = newRoutes;
374
- this.routeMatcher = new RouteMatcher(newRoutes);
375
- this.routeRenderer = new RouteRenderer(this.routeCache);
256
+ matchRoute(path) {
257
+ const exactMatch = this.pathToRouteMap.get(path);
258
+ if (exactMatch) {
259
+ if (exactMatch.parentRoute) {
260
+ return {
261
+ route: exactMatch.parentRoute,
262
+ params: {},
263
+ childRoute: exactMatch
264
+ };
265
+ }
266
+ return { route: exactMatch, params: {} };
376
267
  }
377
-
378
- await this.init();
268
+
269
+ for (const [routePattern, route] of this.pathToRouteMap.entries()) {
270
+ if (routePattern.includes('${')) {
271
+ const { regex, paramNames } = this.compilePathPattern(routePattern);
272
+ const match = path.match(regex);
273
+ if (match) {
274
+ const params = {};
275
+ paramNames.forEach((name, i) => {
276
+ params[name] = match[i + 1];
277
+ });
278
+
279
+ if (route.parentRoute) {
280
+ return {
281
+ route: route.parentRoute,
282
+ params: params,
283
+ childRoute: route
284
+ };
285
+ }
286
+
287
+ return { route, params };
288
+ }
289
+ }
290
+ }
291
+
292
+ const notFoundRoute = this.pathToRouteMap.get('/404');
293
+ return { route: notFoundRoute, params: {} };
294
+ }
295
+
296
+ compilePathPattern(pattern) {
297
+ const paramNames = [];
298
+ const regexPattern = '^' + pattern.replace(/\$\{([^}]+)\}/g, (_, paramName) => {
299
+ paramNames.push(paramName);
300
+ return '([^/]+)';
301
+ }) + '$';
302
+
303
+ return { regex: new RegExp(regexPattern), paramNames };
379
304
  }
380
305
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slicejs-web-framework",
3
- "version": "1.0.33",
3
+ "version": "1.0.34",
4
4
  "description": "",
5
5
  "engines": {
6
6
  "node": ">=20"