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.
@@ -1,301 +1,380 @@
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
+ */
1
12
  export default class Router {
2
13
  constructor(routes) {
3
14
  this.routes = routes;
4
15
  this.activeRoute = null;
5
- this.pathToRouteMap = this.createPathToRouteMap(routes);
6
16
 
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
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);
11
22
 
12
- // NUEVO: Observer para invalidar caché automáticamente
13
- this.setupMutationObserver();
23
+ // Observer para cambios DOM
24
+ this.mutationObserver = null;
25
+
26
+ // Estado del router
27
+ this.isInitialized = false;
28
+ this.isNavigating = false;
14
29
  }
15
30
 
31
+ /**
32
+ * Inicializar router con observadores optimizados
33
+ */
16
34
  async init() {
17
- await this.loadInitialRoute();
18
- window.addEventListener('popstate', this.onRouteChange.bind(this));
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
+ }
19
57
  }
20
58
 
21
- // NUEVO: Observer para detectar cambios en el DOM
59
+ /**
60
+ * Configurar observador de mutaciones optimizado
61
+ */
22
62
  setupMutationObserver() {
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
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();
54
90
  });
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');
55
98
  }
56
99
  }
57
100
 
58
- // NUEVO: Invalidar caché
59
- invalidateCache() {
60
- this.routeContainersCache.clear();
61
- this.lastCacheUpdate = 0;
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
+ });
62
117
  }
63
118
 
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
- }
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;
142
+ }
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;
90
157
  }
91
-
92
- return pathToRouteMap;
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;
93
166
  }
94
167
 
95
- // OPTIMIZADO: Sistema de caché inteligente
96
- async renderRoutesComponentsInPage(searchContainer = document) {
97
- let routerContainersFlag = false;
98
- const routeContainers = this.getCachedRouteContainers(searchContainer);
168
+ /**
169
+ * Manejar cambio de ruta con throttling optimizado
170
+ */
171
+ async onRouteChange() {
172
+ if (this.isNavigating) {
173
+ return;
174
+ }
99
175
 
100
- for (const routeContainer of routeContainers) {
176
+ return this.eventThrottler.throttle('route-change', async () => {
177
+ this.isNavigating = true;
178
+
101
179
  try {
102
- // Verificar que el componente aún esté conectado al DOM
103
- if (!routeContainer.isConnected) {
104
- this.invalidateCache();
105
- continue;
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;
106
187
  }
107
188
 
108
- let response = await routeContainer.renderIfCurrentRoute();
109
- if (response) {
110
- this.activeRoute = routeContainer.props;
111
- routerContainersFlag = true;
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);
112
193
  }
194
+
113
195
  } catch (error) {
114
- slice.logger.logError('Router', `Error rendering route container`, error);
196
+ slice.logger.logError('Router', 'Error during route change', error);
197
+ } finally {
198
+ this.isNavigating = false;
115
199
  }
116
- }
117
-
118
- return routerContainersFlag;
200
+ }, 10);
119
201
  }
120
202
 
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);
203
+ /**
204
+ * Navegar a una ruta específica
205
+ */
206
+ async navigate(path, options = {}) {
207
+ if (!path || path === window.location.pathname) {
208
+ return;
130
209
  }
131
210
 
132
- // Regenerar caché
133
- const routeContainers = this.findAllRouteContainersOptimized(container);
134
- this.routeContainersCache.set(containerKey, routeContainers);
135
- this.lastCacheUpdate = now;
136
-
137
- return routeContainers;
138
- }
139
-
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
- }
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);
156
219
  }
157
- );
158
-
159
- let node;
160
- while (node = walker.nextNode()) {
161
- routeContainers.push(node);
220
+
221
+ // Ejecutar cambio de ruta
222
+ await this.onRouteChange();
223
+
224
+ } catch (error) {
225
+ slice.logger.logError('Router', `Error navigating to ${path}`, error);
162
226
  }
163
-
164
- return routeContainers;
165
227
  }
166
228
 
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
- }
229
+ /**
230
+ * Navegar hacia atrás
231
+ */
232
+ back() {
233
+ window.history.back();
234
+ }
173
235
 
174
- return await this.renderRoutesComponentsInPage(component);
236
+ /**
237
+ * Navegar hacia adelante
238
+ */
239
+ forward() {
240
+ window.history.forward();
175
241
  }
176
242
 
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);
243
+ /**
244
+ * Cargar ruta inicial
245
+ */
246
+ async loadInitialRoute() {
247
+ const path = window.location.pathname;
248
+ const { route, params } = this.routeMatcher.matchRoute(path);
249
+
250
+ if (route) {
251
+ await this.routeRenderer.handleRoute(route, params);
252
+ } else {
253
+ slice.logger.logWarning('Router', `No route found for initial path: ${path}`);
182
254
  }
255
+ }
183
256
 
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();
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);
264
+ }
188
265
 
189
- if (routeContainersFlag) {
190
- return;
191
- }
266
+ hasRoute(path) {
267
+ return this.routeMatcher.hasRoute(path);
268
+ }
192
269
 
193
- const { route, params } = this.matchRoute(path);
194
- if (route) {
195
- await this.handleRoute(route, params);
196
- }
197
- }, 10);
270
+ generateUrl(routePath, params) {
271
+ return this.routeMatcher.generateUrl(routePath, params);
198
272
  }
199
273
 
200
- async navigate(path) {
201
- window.history.pushState({}, path, window.location.origin + path);
202
- await this.onRouteChange();
274
+ // Acceso al caché
275
+ invalidateCache() {
276
+ this.routeCache.invalidateAll();
203
277
  }
204
278
 
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);
279
+ getCacheStats() {
280
+ return this.routeCache.getStats();
281
+ }
212
282
 
213
- if (slice.loading) {
214
- slice.loading.start();
215
- }
283
+ // Acceso al renderer
284
+ async renderRoutesInComponent(component) {
285
+ return this.routeRenderer.renderRoutesInComponent(component);
286
+ }
216
287
 
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
- } else {
225
- const component = await slice.build(componentName, {
226
- params,
227
- sliceId: sliceId,
228
- });
229
- targetElement.innerHTML = '';
230
- targetElement.appendChild(component);
231
- }
288
+ getRendererStats() {
289
+ return this.routeRenderer.getStats();
290
+ }
232
291
 
233
- // Invalidar caché después de cambios importantes en el DOM
292
+ /**
293
+ * Actualizar rutas dinámicamente
294
+ */
295
+ updateRoutes(newRoutes) {
296
+ this.routes = newRoutes;
297
+ this.routeMatcher.updateRoutes(newRoutes);
234
298
  this.invalidateCache();
235
- await this.renderRoutesComponentsInPage();
236
-
237
- if (slice.loading) {
238
- slice.loading.stop();
239
- }
299
+ }
240
300
 
241
- slice.router.activeRoute = route;
301
+ /**
302
+ * Añadir ruta individual
303
+ */
304
+ addRoute(route, basePath = '') {
305
+ this.routeMatcher.addRoute(route, basePath);
306
+ this.invalidateCache();
242
307
  }
243
308
 
244
- async loadInitialRoute() {
245
- const path = window.location.pathname;
246
- const { route, params } = this.matchRoute(path);
309
+ /**
310
+ * Remover ruta
311
+ */
312
+ removeRoute(path) {
313
+ this.routeMatcher.removeRoute(path);
314
+ this.invalidateCache();
315
+ }
247
316
 
248
- await this.handleRoute(route, params);
249
- await this.renderRoutesComponentsInPage();
317
+ /**
318
+ * Obtener todas las rutas
319
+ */
320
+ getAllRoutes() {
321
+ return this.routeMatcher.getAllRoutes();
250
322
  }
251
323
 
252
- matchRoute(path) {
253
- const exactMatch = this.pathToRouteMap.get(path);
254
- if (exactMatch) {
255
- if (exactMatch.parentRoute) {
256
- return {
257
- route: exactMatch.parentRoute,
258
- params: {},
259
- childRoute: exactMatch
260
- };
261
- }
262
- return { route: exactMatch, params: {} };
263
- }
264
-
265
- for (const [routePattern, route] of this.pathToRouteMap.entries()) {
266
- if (routePattern.includes('${')) {
267
- const { regex, paramNames } = this.compilePathPattern(routePattern);
268
- const match = path.match(regex);
269
- if (match) {
270
- const params = {};
271
- paramNames.forEach((name, i) => {
272
- params[name] = match[i + 1];
273
- });
274
-
275
- if (route.parentRoute) {
276
- return {
277
- route: route.parentRoute,
278
- params: params,
279
- childRoute: route
280
- };
281
- }
282
-
283
- return { route, params };
284
- }
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
285
337
  }
286
- }
287
-
288
- const notFoundRoute = this.pathToRouteMap.get('/404');
289
- return { route: notFoundRoute, params: {} };
338
+ };
290
339
  }
291
340
 
292
- compilePathPattern(pattern) {
293
- const paramNames = [];
294
- const regexPattern = '^' + pattern.replace(/\$\{([^}]+)\}/g, (_, paramName) => {
295
- paramNames.push(paramName);
296
- return '([^/]+)';
297
- }) + '$';
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
+ }
350
+
351
+ // Cancelar eventos pendientes
352
+ this.eventThrottler.destroy();
353
+
354
+ // Limpiar subsistemas
355
+ this.routeCache.destroy();
356
+ this.routeRenderer.destroy();
298
357
 
299
- return { regex: new RegExp(regexPattern), paramNames };
358
+ // Remover listeners
359
+ window.removeEventListener('popstate', this.onRouteChange);
360
+
361
+ this.isInitialized = false;
362
+
363
+ slice.logger.logInfo('Router', 'Router destroyed successfully');
364
+ }
365
+
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);
376
+ }
377
+
378
+ await this.init();
300
379
  }
301
380
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slicejs-web-framework",
3
- "version": "1.0.26",
3
+ "version": "1.0.30",
4
4
  "description": "",
5
5
  "engines": {
6
6
  "node": ">=20"