slicejs-web-framework 1.0.25 → 1.0.28

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.
@@ -3,6 +3,14 @@ export default class Router {
3
3
  this.routes = routes;
4
4
  this.activeRoute = null;
5
5
  this.pathToRouteMap = this.createPathToRouteMap(routes);
6
+
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
11
+
12
+ // NUEVO: Observer para invalidar caché automáticamente
13
+ this.setupMutationObserver();
6
14
  }
7
15
 
8
16
  async init() {
@@ -10,13 +18,55 @@ export default class Router {
10
18
  window.addEventListener('popstate', this.onRouteChange.bind(this));
11
19
  }
12
20
 
21
+ // NUEVO: Observer para detectar cambios en el DOM
22
+ 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
54
+ });
55
+ }
56
+ }
57
+
58
+ // NUEVO: Invalidar caché
59
+ invalidateCache() {
60
+ this.routeContainersCache.clear();
61
+ this.lastCacheUpdate = 0;
62
+ }
63
+
13
64
  createPathToRouteMap(routes, basePath = '', parentRoute = null) {
14
65
  const pathToRouteMap = new Map();
15
66
 
16
67
  for (const route of routes) {
17
68
  const fullPath = `${basePath}${route.path}`.replace(/\/+/g, '/');
18
69
 
19
- // Guardar la referencia a la ruta padre
20
70
  const routeWithParent = {
21
71
  ...route,
22
72
  fullPath,
@@ -25,13 +75,12 @@ export default class Router {
25
75
  };
26
76
 
27
77
  pathToRouteMap.set(fullPath, routeWithParent);
28
-
29
- // Si tiene rutas secundarias, también agregarlas al mapa
78
+
30
79
  if (route.children) {
31
80
  const childPathToRouteMap = this.createPathToRouteMap(
32
81
  route.children,
33
82
  fullPath,
34
- routeWithParent // Pasar la ruta actual como padre
83
+ routeWithParent
35
84
  );
36
85
 
37
86
  for (const [childPath, childRoute] of childPathToRouteMap.entries()) {
@@ -43,32 +92,109 @@ export default class Router {
43
92
  return pathToRouteMap;
44
93
  }
45
94
 
46
- async renderRoutesComponentsInPage() {
47
- const routeContainers = document.querySelectorAll('slice-route, slice-multi-route');
95
+ // OPTIMIZADO: Sistema de caché inteligente
96
+ async renderRoutesComponentsInPage(searchContainer = document) {
48
97
  let routerContainersFlag = false;
98
+ const routeContainers = this.getCachedRouteContainers(searchContainer);
49
99
 
50
100
  for (const routeContainer of routeContainers) {
51
- let response = await routeContainer.renderIfCurrentRoute();
52
- if (response) {
53
- this.activeRoute = routeContainer.props;
54
- routerContainersFlag = true;
101
+ try {
102
+ // Verificar que el componente aún esté conectado al DOM
103
+ if (!routeContainer.isConnected) {
104
+ this.invalidateCache();
105
+ continue;
106
+ }
107
+
108
+ let response = await routeContainer.renderIfCurrentRoute();
109
+ if (response) {
110
+ this.activeRoute = routeContainer.props;
111
+ routerContainersFlag = true;
112
+ }
113
+ } catch (error) {
114
+ slice.logger.logError('Router', `Error rendering route container`, error);
55
115
  }
56
116
  }
117
+
57
118
  return routerContainersFlag;
58
119
  }
59
120
 
60
- async onRouteChange() {
61
- const path = window.location.pathname;
62
- const routeContainersFlag = await this.renderRoutesComponentsInPage();
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
+ }
63
131
 
64
- if (routeContainersFlag) {
65
- return;
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
+ }
156
+ }
157
+ );
158
+
159
+ let node;
160
+ while (node = walker.nextNode()) {
161
+ routeContainers.push(node);
66
162
  }
67
163
 
68
- const { route, params } = this.matchRoute(path);
69
- if (route) {
70
- await this.handleRoute(route, params);
164
+ return routeContainers;
165
+ }
166
+
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
+ }
173
+
174
+ return await this.renderRoutesComponentsInPage(component);
175
+ }
176
+
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);
71
182
  }
183
+
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();
188
+
189
+ if (routeContainersFlag) {
190
+ return;
191
+ }
192
+
193
+ const { route, params } = this.matchRoute(path);
194
+ if (route) {
195
+ await this.handleRoute(route, params);
196
+ }
197
+ }, 10);
72
198
  }
73
199
 
74
200
  async navigate(path) {
@@ -77,70 +203,69 @@ export default class Router {
77
203
  }
78
204
 
79
205
  async handleRoute(route, params) {
80
- const targetElement = document.querySelector('#app');
81
-
82
- // Si tenemos una ruta con parentRoute, usamos el componente del padre
83
- const componentName = route.parentRoute ? route.parentRoute.component : route.component;
84
- const sliceId = `route-${componentName}`;
85
-
86
- const existingComponent = slice.controller.getComponent(sliceId);
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);
87
212
 
88
- if (slice.loading) {
89
- slice.loading.start();
90
- }
213
+ if (slice.loading) {
214
+ slice.loading.start();
215
+ }
91
216
 
92
- if (existingComponent) {
93
- targetElement.innerHTML = '';
94
- if (existingComponent.update) {
95
- existingComponent.props = { ...existingComponent.props, ...params };
96
- await existingComponent.update();
97
- }
98
- await this.renderRoutesComponentsInPage();
99
- targetElement.appendChild(existingComponent);
100
- } else {
101
- const component = await slice.build(componentName, {
102
- params,
103
- sliceId: sliceId,
104
- });
105
- await this.renderRoutesComponentsInPage();
106
- targetElement.innerHTML = '';
107
- targetElement.appendChild(component);
217
+ if (existingComponent) {
218
+ targetElement.innerHTML = '';
219
+ if (existingComponent.update) {
220
+ existingComponent.props = { ...existingComponent.props, ...params };
221
+ await existingComponent.update();
108
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
+ });
109
231
 
232
+ targetElement.innerHTML = '';
233
+ targetElement.appendChild(component);
234
+
235
+ // Renderizar INMEDIATAMENTE después de insertar
236
+ await this.renderRoutesInComponent(component);
237
+ }
110
238
 
239
+ // Invalidar caché después de cambios importantes en el DOM
240
+ this.invalidateCache();
111
241
 
112
- if (slice.loading) {
113
- slice.loading.stop();
114
- }
115
-
116
- slice.router.activeRoute = route;
242
+ if (slice.loading) {
243
+ slice.loading.stop();
117
244
  }
118
245
 
246
+ slice.router.activeRoute = route;
247
+ }
248
+
119
249
  async loadInitialRoute() {
120
250
  const path = window.location.pathname;
121
251
  const { route, params } = this.matchRoute(path);
122
252
 
123
253
  await this.handleRoute(route, params);
124
- await this.renderRoutesComponentsInPage();
125
254
  }
126
255
 
127
256
  matchRoute(path) {
128
- // 1. Buscar coincidencia exacta en el mapa
129
257
  const exactMatch = this.pathToRouteMap.get(path);
130
258
  if (exactMatch) {
131
- // Si es una ruta hija y tiene un padre definido, devolvemos el padre en su lugar
132
259
  if (exactMatch.parentRoute) {
133
- // Mantenemos la información de parámetros que viene de la ruta actual
134
260
  return {
135
261
  route: exactMatch.parentRoute,
136
262
  params: {},
137
- childRoute: exactMatch // Guardamos la referencia a la ruta hija original
263
+ childRoute: exactMatch
138
264
  };
139
265
  }
140
266
  return { route: exactMatch, params: {} };
141
267
  }
142
268
 
143
- // 2. Buscar coincidencias en rutas con parámetros
144
269
  for (const [routePattern, route] of this.pathToRouteMap.entries()) {
145
270
  if (routePattern.includes('${')) {
146
271
  const { regex, paramNames } = this.compilePathPattern(routePattern);
@@ -151,7 +276,6 @@ export default class Router {
151
276
  params[name] = match[i + 1];
152
277
  });
153
278
 
154
- // Si es una ruta hija, devolvemos el padre con los parámetros
155
279
  if (route.parentRoute) {
156
280
  return {
157
281
  route: route.parentRoute,
@@ -165,7 +289,6 @@ export default class Router {
165
289
  }
166
290
  }
167
291
 
168
- // 3. Si no hay coincidencias, retornar la ruta 404
169
292
  const notFoundRoute = this.pathToRouteMap.get('/404');
170
293
  return { route: notFoundRoute, params: {} };
171
294
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slicejs-web-framework",
3
- "version": "1.0.25",
3
+ "version": "1.0.28",
4
4
  "description": "",
5
5
  "engines": {
6
6
  "node": ">=20"