jerkjs 2.5.4 → 2.5.8

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.
Files changed (69) hide show
  1. package/BENCHMARK_RESULTS.md +60 -0
  2. package/CHANGELOG.md +43 -0
  3. package/ESTADISTICAS_RENDIMIENTO.md +106 -0
  4. package/README.md +142 -423
  5. package/README_LEGACY.md +513 -0
  6. package/debug_hook.js +11 -0
  7. package/doc-2.5/ADMIN_EXTENSION_COMMANDS_MANUAL.md +261 -0
  8. package/doc-2.5/ADMIN_EXTENSION_HOOK_EXAMPLE.md +28 -0
  9. package/doc-2.5/ADMIN_EXTENSION_INTEGRATION_MANUAL.md +232 -0
  10. package/doc-2.5/CACHE_SYSTEM_MAP.md +206 -0
  11. package/doc-2.5/SESSION_SECURITY_FLAGS.md +174 -0
  12. package/doc-2.5/an/303/241lisis-completo-jerk-framework.md +213 -0
  13. package/docs/CACHE_SYSTEM_MAP.md +206 -0
  14. package/docs/SERVER_OPTIMIZATION_NOTES.md +87 -0
  15. package/index.js +7 -1
  16. package/jerk2.5.webp +0 -0
  17. package/lib/admin/AdminExtension.js +436 -0
  18. package/lib/admin/ModuleLoader.js +77 -0
  19. package/lib/admin/config.js +21 -0
  20. package/lib/admin/modules/CacheModule.js +145 -0
  21. package/lib/admin/modules/STATS_MODULE_README.md +98 -0
  22. package/lib/admin/modules/StatsModule.js +140 -0
  23. package/lib/admin/modules/SystemModule.js +140 -0
  24. package/lib/admin/modules/TimeModule.js +95 -0
  25. package/lib/cache/CacheHooks.js +141 -0
  26. package/lib/core/server.js +199 -46
  27. package/lib/middleware/session.js +11 -3
  28. package/lib/mvc/viewEngine.js +26 -1
  29. package/lib/router/RouteMatcher.js +242 -54
  30. package/lib/utils/globalStats.js +16 -0
  31. package/package.json +2 -2
  32. package/@qaLoadModel/controllers/ProductController.js +0 -143
  33. package/@qaLoadModel/controllers/UserController.js +0 -143
  34. package/@qaLoadModel/models/ProductModel.js +0 -41
  35. package/@qaLoadModel/models/UserModel.js +0 -41
  36. package/@qaLoadModel/package.json +0 -22
  37. package/@qaLoadModel/qa_report.md +0 -71
  38. package/@qaLoadModel/results.md +0 -97
  39. package/@qaLoadModel/routes.json +0 -58
  40. package/@qaLoadModel/server.js +0 -43
  41. package/@qaLoadModel/simple-test.js +0 -96
  42. package/@qaLoadModel/test-models.js +0 -144
  43. package/@qaLoadModel/test_endpoints.sh +0 -35
  44. package/@qaLoadModel/test_final.js +0 -89
  45. package/@qaLoadModel/views/products/index.html +0 -45
  46. package/@qaLoadModel/views/products/show.html +0 -27
  47. package/@qaLoadModel/views/users/index.html +0 -44
  48. package/@qaLoadModel/views/users/show.html +0 -26
  49. package/qa/INFORME_QA_JERKJS_ROUTING.md +0 -108
  50. package/qa/informe_qa_fix_enrutamiento.md +0 -93
  51. package/qa-app/controllers/homeController.js +0 -9
  52. package/qa-app/controllers/userController.js +0 -76
  53. package/qa-app/hooks-config.js +0 -65
  54. package/qa-app/models/UserModel.js +0 -36
  55. package/qa-app/package-lock.json +0 -1683
  56. package/qa-app/package.json +0 -25
  57. package/qa-app/public/css/style.css +0 -15
  58. package/qa-app/public/images/logo.png +0 -3
  59. package/qa-app/public/index.html +0 -15
  60. package/qa-app/public/js/main.js +0 -7
  61. package/qa-app/routes/api-routes.json +0 -23
  62. package/qa-app/routes/page-routes.json +0 -16
  63. package/qa-app/routes/static-routes.json +0 -20
  64. package/qa-app/server.js +0 -68
  65. package/qa-app/views/footer.html +0 -3
  66. package/qa-app/views/index.html +0 -20
  67. package/qa-app/views/users.html +0 -20
  68. package/utils/find_file_path.sh +0 -36
  69. /package/{doc2.5.3 → doc-2.5}/manual-mvc-completo.md +0 -0
@@ -6,13 +6,24 @@
6
6
 
7
7
  const http = require('http');
8
8
  const https = require('https');
9
- const url = require('url');
9
+ const urlModule = require('url'); // Renombrado para evitar conflicto con variable local
10
10
  const fs = require('fs');
11
11
  const path = require('path');
12
12
  const { Logger } = require('../utils/logger');
13
13
  const { ErrorHandler } = require('../utils/errorHandler');
14
14
  const { getMimeType } = require('../utils/mimeType');
15
15
  const RouteMatcher = require('../router/RouteMatcher');
16
+ const AdminExtension = require('../admin/AdminExtension');
17
+ const { globalStats } = require('../utils/globalStats');
18
+
19
+ // Carga anticipada de módulos comunes para evitar cargas repetidas
20
+ let hooksInstance = null;
21
+ const getHooks = () => {
22
+ if (!hooksInstance) {
23
+ hooksInstance = require('../../index.js').hooks;
24
+ }
25
+ return hooksInstance;
26
+ };
16
27
 
17
28
  class APIServer {
18
29
  /**
@@ -50,6 +61,42 @@ class APIServer {
50
61
 
51
62
  // Inicializar el componente de enrutamiento
52
63
  this.routeMatcher = new RouteMatcher();
64
+
65
+ // Inicializar la extensión de administración
66
+ this.adminExtension = null;
67
+
68
+ // Inicializar el sistema de hooks si no existe
69
+ if (!options.hooks) {
70
+ const { hooks } = require('../../index.js'); // Ruta corregida
71
+ this.hooks = hooks;
72
+ } else {
73
+ this.hooks = options.hooks;
74
+ }
75
+
76
+ // Conectar el sistema de hooks al routeMatcher
77
+ this.routeMatcher.hooks = this.hooks;
78
+
79
+ // Propiedad para almacenar el viewEngine
80
+ this._viewEngine = null;
81
+ }
82
+
83
+ /**
84
+ * Setter para el viewEngine que también conecta los hooks
85
+ */
86
+ set viewEngine(engine) {
87
+ this._viewEngine = engine;
88
+
89
+ // Conectar el sistema de hooks al viewEngine si está disponible
90
+ if (this._viewEngine && this.hooks) {
91
+ this._viewEngine.hooks = this.hooks;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Getter para el viewEngine
97
+ */
98
+ get viewEngine() {
99
+ return this._viewEngine;
53
100
  }
54
101
 
55
102
  /**
@@ -59,10 +106,15 @@ class APIServer {
59
106
  * @returns {Function} - Handler para servir archivos estáticos
60
107
  */
61
108
  createStaticFileHandler(staticConfig, routePath) {
109
+ // Pre-calcular valores comunes para evitar cálculos repetidos
110
+ const baseDir = path.isAbsolute(staticConfig.dir)
111
+ ? staticConfig.dir
112
+ : path.resolve(process.cwd(), staticConfig.dir);
113
+
62
114
  return async (req, res) => {
63
115
  try {
64
116
  // Disparar hook antes de procesar archivo estático
65
- const hooks = require('../../index.js').hooks;
117
+ const hooks = getHooks();
66
118
  if (hooks) {
67
119
  const hookResult = hooks.applyFilters('pre_static_file_serve', {
68
120
  req,
@@ -83,16 +135,16 @@ class APIServer {
83
135
  // Extraer la subruta de la solicitud
84
136
  // Si la ruta base es /static y la solicitud es /static/assets/hola.js
85
137
  // entonces la subruta sería assets/hola.js
86
- const parsedUrl = require('url').parse(req.url, true);
138
+ const parsedUrl = urlModule.parse(req.url, true);
87
139
  const pathname = parsedUrl.pathname;
88
-
140
+
89
141
  // Extraer la subruta usando la ruta base conocida para esta ruta estática
90
142
  let requestedPath = pathname.substring(routePath.length);
91
143
  if (requestedPath.startsWith('/')) {
92
144
  requestedPath = requestedPath.substring(1);
93
145
  }
94
146
 
95
- // Prevenir path traversal
147
+ // Prevenir path traversal - optimizado para evitar operaciones redundantes
96
148
  const normalizedPath = path.normalize(requestedPath);
97
149
  if (normalizedPath.includes('..')) {
98
150
  res.writeHead(403, { 'Content-Type': 'application/json' });
@@ -104,12 +156,6 @@ class APIServer {
104
156
  return;
105
157
  }
106
158
 
107
- // Asegurarse de que el directorio base sea absoluto
108
- let baseDir = staticConfig.dir;
109
- if (!path.isAbsolute(baseDir)) {
110
- baseDir = path.resolve(process.cwd(), baseDir);
111
- }
112
-
113
159
  // Construir la ruta física
114
160
  const physicalPath = path.join(baseDir, normalizedPath);
115
161
 
@@ -262,7 +308,7 @@ class APIServer {
262
308
  res.writeHead(404, { 'Content-Type': 'application/json' });
263
309
  res.end(JSON.stringify({ error: 'Archivo no encontrado', path: error.path }));
264
310
 
265
- const hooks = require('../../index.js').hooks;
311
+ const hooks = getHooks();
266
312
  if (hooks) {
267
313
  hooks.doAction('static_file_not_found', error.path, req, res);
268
314
  }
@@ -270,7 +316,7 @@ class APIServer {
270
316
  res.writeHead(403, { 'Content-Type': 'application/json' });
271
317
  res.end(JSON.stringify({ error: 'Acceso denegado', path: error.path }));
272
318
 
273
- const hooks = require('../../index.js').hooks;
319
+ const hooks = getHooks();
274
320
  if (hooks) {
275
321
  hooks.doAction('static_file_access_denied', error.path, req, res);
276
322
  }
@@ -279,7 +325,7 @@ class APIServer {
279
325
  const { ErrorHandler } = require('../utils/errorHandler');
280
326
  ErrorHandler.handle(error, req, res, this.logger);
281
327
 
282
- const hooks = require('../../index.js').hooks;
328
+ const hooks = getHooks();
283
329
  if (hooks) {
284
330
  hooks.doAction('static_file_error', error, req, res);
285
331
  }
@@ -345,12 +391,18 @@ class APIServer {
345
391
  // Crear handler para archivos estáticos
346
392
  const staticHandler = this.createStaticFileHandler(routeConfig.static, routeConfig.path);
347
393
 
348
- this.routes.push({
394
+ const route = {
349
395
  method: routeConfig.method.toUpperCase(),
350
396
  path: routeConfig.path,
351
397
  handler: staticHandler,
352
398
  isStatic: true // Marcar como ruta estática para posible procesamiento especial
353
- });
399
+ };
400
+ this.routes.push(route);
401
+
402
+ // Disparar hook cuando se registra una ruta
403
+ if (this.hooks) {
404
+ this.hooks.doAction('route_registered', route);
405
+ }
354
406
 
355
407
  return; // Salir después de procesar ruta estática
356
408
  }
@@ -429,11 +481,17 @@ class APIServer {
429
481
  };
430
482
 
431
483
  // Agregar la ruta con el handler autenticado
432
- this.routes.push({
484
+ const route = {
433
485
  method: routeConfig.method.toUpperCase(),
434
486
  path: routeConfig.path,
435
487
  handler: authenticatedHandler
436
- });
488
+ };
489
+ this.routes.push(route);
490
+
491
+ // Disparar hook cuando se registra una ruta
492
+ if (this.hooks) {
493
+ this.hooks.doAction('route_registered', route);
494
+ }
437
495
  } else {
438
496
  // Si no hay sessionManager en el servidor, agregar la ruta normalmente
439
497
  this.routes.push({
@@ -480,27 +538,45 @@ class APIServer {
480
538
  };
481
539
 
482
540
  // Agregar la ruta con el handler autenticado
483
- this.routes.push({
541
+ const route = {
484
542
  method: routeConfig.method.toUpperCase(),
485
543
  path: routeConfig.path,
486
544
  handler: authenticatedHandler
487
- });
545
+ };
546
+ this.routes.push(route);
547
+
548
+ // Disparar hook cuando se registra una ruta
549
+ if (this.hooks) {
550
+ this.hooks.doAction('route_registered', route);
551
+ }
488
552
  } else {
489
553
  // Si no hay authenticator en el servidor, agregar la ruta normalmente
490
- this.routes.push({
554
+ const route = {
491
555
  method: routeConfig.method.toUpperCase(),
492
556
  path: routeConfig.path,
493
557
  handler: finalHandler
494
- });
558
+ };
559
+ this.routes.push(route);
560
+
561
+ // Disparar hook cuando se registra una ruta
562
+ if (this.hooks) {
563
+ this.hooks.doAction('route_registered', route);
564
+ }
495
565
  }
496
566
  }
497
567
  } else {
498
568
  // Si no hay autenticación requerida, agregar la ruta normalmente
499
- this.routes.push({
569
+ const route = {
500
570
  method: routeConfig.method.toUpperCase(),
501
571
  path: routeConfig.path,
502
572
  handler: finalHandler
503
- });
573
+ };
574
+ this.routes.push(route);
575
+
576
+ // Disparar hook cuando se registra una ruta
577
+ if (this.hooks) {
578
+ this.hooks.doAction('route_registered', route);
579
+ }
504
580
  }
505
581
  }
506
582
 
@@ -527,7 +603,7 @@ class APIServer {
527
603
  */
528
604
  start() {
529
605
  // Disparar hook antes de iniciar el servidor
530
- const hooks = require('../../index.js').hooks;
606
+ const hooks = getHooks();
531
607
  if (hooks) {
532
608
  hooks.doAction('pre_server_start', this);
533
609
  }
@@ -571,12 +647,28 @@ class APIServer {
571
647
  // Disparar hook después de iniciar el servidor
572
648
  if (hooks) {
573
649
  hooks.doAction('post_server_start', this);
650
+
651
+ // Disparar hook para inicializar extensiones después de que el servidor esté completamente iniciado
652
+ hooks.doAction('admin_extensions_initialize', this);
574
653
  }
575
654
  });
576
655
 
577
656
  return this.server;
578
657
  }
579
658
 
659
+ /**
660
+ * Inicializa la extensión de administración
661
+ * @param {Object} options - Opciones para la extensión de administración
662
+ */
663
+ initializeAdminExtension(options = {}) {
664
+ if (!this.adminExtension) {
665
+ this.adminExtension = new AdminExtension(options);
666
+ this.adminExtension.initialize(this);
667
+ }
668
+
669
+ return this.adminExtension;
670
+ }
671
+
580
672
  /**
581
673
  * Detiene el servidor
582
674
  */
@@ -586,6 +678,11 @@ class APIServer {
586
678
  this.logger.info('Servidor detenido');
587
679
  });
588
680
  }
681
+
682
+ // Cerrar la extensión de administración si está activa
683
+ if (this.adminExtension) {
684
+ this.adminExtension.close();
685
+ }
589
686
  }
590
687
 
591
688
  /**
@@ -594,11 +691,11 @@ class APIServer {
594
691
  * @param {Object} res - Objeto de respuesta HTTP
595
692
  */
596
693
  async handleRequest(req, res) {
597
- const parsedUrl = url.parse(req.url, true);
694
+ const parsedUrl = urlModule.parse(req.url, true);
598
695
  const { pathname, query } = parsedUrl;
599
696
 
600
697
  // Disparar hook antes de procesar la solicitud
601
- const hooks = require('../../index.js').hooks;
698
+ const hooks = getHooks();
602
699
  if (hooks) {
603
700
  hooks.doAction('request_received', req, res);
604
701
  }
@@ -635,6 +732,9 @@ class APIServer {
635
732
  // Concatenar todos los chunks una sola vez
636
733
  req.body = Buffer.concat(bodyChunks).toString();
637
734
 
735
+ // Actualizar estadísticas globales de bytes recibidos
736
+ globalStats.requestBytes += bodySize;
737
+
638
738
  // Parsear body si es JSON
639
739
  if (req.headers['content-type'] && req.headers['content-type'].includes('application/json')) {
640
740
  try {
@@ -650,25 +750,29 @@ class APIServer {
650
750
 
651
751
  // Agregar el método render a la respuesta si hay un viewEngine configurado
652
752
  if (this.viewEngine) {
653
- res.render = (viewName, data = {}) => {
654
- try {
655
- const renderedHtml = this.viewEngine.render(viewName, data);
656
- res.writeHead(200, { 'Content-Type': 'text/html' });
657
- res.end(renderedHtml);
753
+ // Definir render solo una vez si no existe
754
+ if (!res._renderDefined) {
755
+ res.render = (viewName, data = {}) => {
756
+ try {
757
+ const renderedHtml = this.viewEngine.render(viewName, data);
758
+ res.writeHead(200, { 'Content-Type': 'text/html' });
759
+ res.end(renderedHtml);
658
760
 
659
- if (hooks) {
660
- hooks.doAction('view_rendered', viewName, data, req, res);
661
- }
662
- } catch (error) {
663
- // Usar el ErrorHandler para mostrar el stacktrace en color amarillo
664
- const { ErrorHandler } = require('../utils/errorHandler');
665
- ErrorHandler.handle(error, req, res, this.logger);
761
+ if (hooks) {
762
+ hooks.doAction('view_rendered', viewName, data, req, res);
763
+ }
764
+ } catch (error) {
765
+ // Usar el ErrorHandler para mostrar el stacktrace en color amarillo
766
+ const { ErrorHandler } = require('../utils/errorHandler');
767
+ ErrorHandler.handle(error, req, res, this.logger);
666
768
 
667
- if (hooks) {
668
- hooks.doAction('view_render_error', viewName, data, req, res, error);
769
+ if (hooks) {
770
+ hooks.doAction('view_render_error', viewName, data, req, res, error);
771
+ }
669
772
  }
670
- }
671
- };
773
+ };
774
+ res._renderDefined = true;
775
+ }
672
776
  }
673
777
 
674
778
  // Hacer que el modelManager esté disponible en la solicitud si está configurado
@@ -790,21 +894,70 @@ class APIServer {
790
894
  }
791
895
  }
792
896
 
897
+ // Capturar el tamaño de la respuesta antes de enviarla
898
+ const originalWrite = res.write;
899
+ const originalEnd = res.end;
900
+ let responseData = [];
901
+
902
+ res.write = function(chunk) {
903
+ if (chunk) {
904
+ responseData.push(Buffer.from(chunk));
905
+ }
906
+ return originalWrite.apply(this, arguments);
907
+ };
908
+
909
+ res.end = function(chunk) {
910
+ if (chunk) {
911
+ responseData.push(Buffer.from(chunk));
912
+ }
913
+
914
+ // Almacenar los datos de la respuesta para estadísticas
915
+ res._data = Buffer.concat(responseData);
916
+
917
+ // Actualizar estadísticas globales de bytes enviados
918
+ if (res._data) {
919
+ const responseSize = Buffer.byteLength(res._data, 'utf8');
920
+ globalStats.responseBytes += responseSize;
921
+ }
922
+
923
+ return originalEnd.apply(this, arguments);
924
+ };
925
+
926
+ // Actualizar estadísticas globales de solicitudes procesadas
927
+ globalStats.requestsProcessed++;
928
+
793
929
  // Ejecutar handler de la ruta
794
930
  await matchedRoute.route.handler(req, res);
795
931
 
932
+ // Actualizar estadísticas globales de respuestas enviadas
933
+ globalStats.responsesSent++;
934
+
935
+ // Registrar acceso a ruta
936
+ if (req.originalUrl) {
937
+ const routeKey = `${req.method} ${req.originalUrl}`;
938
+ if (!globalStats.routeAccesses.has(routeKey)) {
939
+ globalStats.routeAccesses.set(routeKey, 0);
940
+ }
941
+ globalStats.routeAccesses.set(routeKey, globalStats.routeAccesses.get(routeKey) + 1);
942
+ }
943
+
944
+ // Registrar hit al endpoint
945
+ const endpointKey = `${req.method} ${matchedRoute.route.path}`;
946
+ if (!globalStats.endpointHits.has(endpointKey)) {
947
+ globalStats.endpointHits.set(endpointKey, 0);
948
+ }
949
+ globalStats.endpointHits.set(endpointKey, globalStats.endpointHits.get(endpointKey) + 1);
950
+
796
951
  if (hooks) {
797
952
  hooks.doAction('route_handler_executed', matchedRoute, req, res);
798
953
  }
799
954
  } else {
800
955
  // Ruta no encontrada
801
- if (hooks) {
956
+ if (hooks) {
802
957
  hooks.doAction('route_not_found', pathname, req, res);
803
958
  }
804
959
  res.writeHead(404, { 'Content-Type': 'application/json' });
805
960
  res.end(JSON.stringify({ error: 'Ruta no encontrada', path: pathname }));
806
-
807
-
808
961
  }
809
962
  }
810
963
 
@@ -171,8 +171,12 @@ class SessionManager {
171
171
  req.session.id = newSessionId;
172
172
  req.session.data = processedUserData;
173
173
 
174
- // Establecer cookie con el ID de sesión
175
- res.setHeader('Set-Cookie', `${this.cookieName}=${newSessionId}; HttpOnly; Path=/; Max-Age=${this.timeout / 1000}`);
174
+ // Aplicar filtro para permitir modificación de las flags de la cookie
175
+ let cookieFlags = 'HttpOnly; Path=/';
176
+ cookieFlags = this.hooks.applyFilters('session_cookie_flags', cookieFlags, req, res, newSessionId);
177
+
178
+ // Establecer cookie con el ID de sesión y flags personalizadas
179
+ res.setHeader('Set-Cookie', `${this.cookieName}=${newSessionId}; ${cookieFlags}; Max-Age=${this.timeout / 1000}`);
176
180
 
177
181
  // Disparar hook después de crear la sesión
178
182
  this.hooks.doAction('session_created_response', req, res, newSessionId);
@@ -200,8 +204,12 @@ class SessionManager {
200
204
  // Disparar hook antes de destruir la sesión
201
205
  this.hooks.doAction('session_destroy_before', req, res, req.session.id);
202
206
 
207
+ // Aplicar filtro para permitir modificación de las flags de la cookie al destruir la sesión
208
+ let destroyCookieFlags = 'HttpOnly; Path=/';
209
+ destroyCookieFlags = this.hooks.applyFilters('session_cookie_destroy_flags', destroyCookieFlags, req, res, req.session.id);
210
+
203
211
  // Borrar cookie
204
- res.setHeader('Set-Cookie', `${this.cookieName}=; HttpOnly; Path=/; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:01 GMT`);
212
+ res.setHeader('Set-Cookie', `${this.cookieName}=; ${destroyCookieFlags}; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:01 GMT`);
205
213
 
206
214
  const result = this.destroySession(req.session.id);
207
215
  req.session.id = null;
@@ -274,7 +274,22 @@ class ViewEngine {
274
274
 
275
275
  // Si el cache está habilitado, guardar la vista compilada (sin variables)
276
276
  if (this.cacheEnabled) {
277
- this.viewCache.set(viewPath, viewContent);
277
+ // Aplicar filter antes de cachear la vista
278
+ const contentToCache = this.hooks ?
279
+ this.hooks.applyFilters('before_view_cache', viewContent, viewPath) : viewContent;
280
+
281
+ // Verificar con hook si se debe cachear esta vista
282
+ const shouldCache = this.hooks ?
283
+ this.hooks.applyFilters('should_cache_view', true, viewPath, contentToCache) : true;
284
+
285
+ if (shouldCache) {
286
+ this.viewCache.set(viewPath, contentToCache);
287
+
288
+ // Disparar hook después de cachear
289
+ if (this.hooks) {
290
+ this.hooks.doAction('view_cached', viewPath, contentToCache);
291
+ }
292
+ }
278
293
  }
279
294
 
280
295
  // Procesar el template con las variables
@@ -817,7 +832,17 @@ class ViewEngine {
817
832
  * Limpia el cache de vistas
818
833
  */
819
834
  clearCache() {
835
+ // Disparar hook antes de limpiar el caché
836
+ if (this.hooks) {
837
+ this.hooks.doAction('before_view_cache_clear', this);
838
+ }
839
+
820
840
  this.viewCache.clear();
841
+
842
+ // Disparar hook después de limpiar el caché
843
+ if (this.hooks) {
844
+ this.hooks.doAction('view_cache_cleared');
845
+ }
821
846
  }
822
847
  }
823
848