slicejs-web-framework 1.0.28 → 1.0.31

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,305 +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;
90
142
  }
91
-
92
- return pathToRouteMap;
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;
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
  }
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;
116
209
  }
117
210
 
118
- return routerContainersFlag;
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
+ }
119
227
  }
120
228
 
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
- }
229
+ /**
230
+ * Navegar hacia atrás
231
+ */
232
+ back() {
233
+ window.history.back();
234
+ }
131
235
 
132
- // Regenerar caché
133
- const routeContainers = this.findAllRouteContainersOptimized(container);
134
- this.routeContainersCache.set(containerKey, routeContainers);
135
- this.lastCacheUpdate = now;
136
-
137
- return routeContainers;
236
+ /**
237
+ * Navegar hacia adelante
238
+ */
239
+ forward() {
240
+ window.history.forward();
138
241
  }
139
242
 
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
- );
243
+ /**
244
+ * Cargar ruta inicial
245
+ */
246
+ async loadInitialRoute() {
247
+ const path = window.location.pathname;
248
+ const { route, params } = this.routeMatcher.matchRoute(path);
158
249
 
159
- let node;
160
- while (node = walker.nextNode()) {
161
- routeContainers.push(node);
250
+ if (route) {
251
+ await this.routeRenderer.handleRoute(route, params);
252
+ } else {
253
+ slice.logger.logWarning('Router', `No route found for initial path: ${path}`);
162
254
  }
255
+ }
163
256
 
164
- return routeContainers;
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);
165
264
  }
166
265
 
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
- }
266
+ hasRoute(path) {
267
+ return this.routeMatcher.hasRoute(path);
268
+ }
173
269
 
174
- return await this.renderRoutesComponentsInPage(component);
270
+ generateUrl(routePath, params) {
271
+ return this.routeMatcher.generateUrl(routePath, params);
175
272
  }
176
273
 
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
- }
274
+ // Acceso al caché
275
+ invalidateCache() {
276
+ this.routeCache.invalidateAll();
277
+ }
183
278
 
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();
279
+ getCacheStats() {
280
+ return this.routeCache.getStats();
281
+ }
188
282
 
189
- if (routeContainersFlag) {
190
- return;
191
- }
283
+ // Acceso al renderer
284
+ async renderRoutesInComponent(component) {
285
+ return this.routeRenderer.renderRoutesInComponent(component);
286
+ }
192
287
 
193
- const { route, params } = this.matchRoute(path);
194
- if (route) {
195
- await this.handleRoute(route, params);
196
- }
197
- }, 10);
288
+ getRendererStats() {
289
+ return this.routeRenderer.getStats();
198
290
  }
199
291
 
200
- async navigate(path) {
201
- window.history.pushState({}, path, window.location.origin + path);
202
- await this.onRouteChange();
292
+ /**
293
+ * Actualizar rutas dinámicamente
294
+ */
295
+ updateRoutes(newRoutes) {
296
+ this.routes = newRoutes;
297
+ this.routeMatcher.updateRoutes(newRoutes);
298
+ this.invalidateCache();
203
299
  }
204
300
 
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);
301
+ /**
302
+ * Añadir ruta individual
303
+ */
304
+ addRoute(route, basePath = '') {
305
+ this.routeMatcher.addRoute(route, basePath);
306
+ this.invalidateCache();
307
+ }
212
308
 
213
- if (slice.loading) {
214
- slice.loading.start();
309
+ /**
310
+ * Remover ruta
311
+ */
312
+ removeRoute(path) {
313
+ this.routeMatcher.removeRoute(path);
314
+ this.invalidateCache();
215
315
  }
216
316
 
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
- });
317
+ /**
318
+ * Obtener todas las rutas
319
+ */
320
+ getAllRoutes() {
321
+ return this.routeMatcher.getAllRoutes();
322
+ }
231
323
 
232
- targetElement.innerHTML = '';
233
- targetElement.appendChild(component);
234
-
235
- // Renderizar INMEDIATAMENTE después de insertar
236
- await this.renderRoutesInComponent(component);
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
+ };
237
339
  }
238
340
 
239
- // Invalidar caché después de cambios importantes en el DOM
240
- this.invalidateCache();
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
+ }
241
350
 
242
- if (slice.loading) {
243
- slice.loading.stop();
244
- }
351
+ // Cancelar eventos pendientes
352
+ this.eventThrottler.destroy();
245
353
 
246
- slice.router.activeRoute = route;
247
- }
354
+ // Limpiar subsistemas
355
+ this.routeCache.destroy();
356
+ this.routeRenderer.destroy();
248
357
 
249
- async loadInitialRoute() {
250
- const path = window.location.pathname;
251
- const { route, params } = this.matchRoute(path);
358
+ // Remover listeners
359
+ window.removeEventListener('popstate', this.onRouteChange);
252
360
 
253
- await this.handleRoute(route, params);
361
+ this.isInitialized = false;
362
+
363
+ slice.logger.logInfo('Router', 'Router destroyed successfully');
254
364
  }
255
365
 
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: {} };
267
- }
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
- }
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);
290
376
  }
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 };
377
+
378
+ await this.init();
304
379
  }
305
380
  }
package/Slice/Slice.js CHANGED
@@ -11,6 +11,7 @@ export default class Slice {
11
11
  this.loggerConfig = sliceConfig.logger;
12
12
  this.debuggerConfig = sliceConfig.debugger;
13
13
  this.loadingConfig = sliceConfig.loading;
14
+ this.productionConfig = sliceConfig.production;
14
15
  }
15
16
 
16
17
  async getClass(module) {
@@ -22,6 +23,10 @@ export default class Slice {
22
23
  }
23
24
  }
24
25
 
26
+ isProduction(){
27
+ return this.productionConfig.enabled;
28
+ }
29
+
25
30
  getComponent(componentSliceId) {
26
31
  return this.controller.activeComponents.get(componentSliceId);
27
32
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slicejs-web-framework",
3
- "version": "1.0.28",
3
+ "version": "1.0.31",
4
4
  "description": "",
5
5
  "engines": {
6
6
  "node": ">=20"