kukuy 1.4.0 → 1.6.0

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,8 +3,12 @@ const { HealthChecker } = require('../utils/HealthChecker');
3
3
  class ServerPool {
4
4
  constructor() {
5
5
  this.servers = [];
6
+ this.healthyServers = []; // Caché de servidores saludables
7
+ this.serversById = new Map(); // Acceso rápido por ID
8
+ this.serversByTag = new Map(); // Caché de servidores por tag
6
9
  this.healthChecker = new HealthChecker();
7
10
  this.nextId = 1;
11
+ this.cacheValid = false; // Indicador de validez de caché
8
12
  }
9
13
 
10
14
  addServer(serverConfig) {
@@ -22,6 +26,12 @@ class ServerPool {
22
26
  };
23
27
 
24
28
  this.servers.push(server);
29
+ this.serversById.set(server.id, server);
30
+
31
+ // Actualizar cachés
32
+ this.updateTagCache(server);
33
+ this.cacheValid = false;
34
+
25
35
  return server;
26
36
  }
27
37
 
@@ -36,19 +46,39 @@ class ServerPool {
36
46
  }
37
47
 
38
48
  getHealthyServers() {
39
- return this.servers.filter(server => server.healthy && server.active);
49
+ if (!this.cacheValid) {
50
+ this.healthyServers = this.servers.filter(server => server.healthy && server.active);
51
+ this.cacheValid = true;
52
+ }
53
+ return this.healthyServers;
40
54
  }
41
55
 
42
56
  getServersByTag(tag) {
43
- return this.servers.filter(server => server.tags.includes(tag));
57
+ if (this.serversByTag.has(tag)) {
58
+ return this.serversByTag.get(tag);
59
+ }
60
+ return [];
61
+ }
62
+
63
+ // Actualiza la caché de servidores por tag
64
+ updateTagCache(server) {
65
+ for (const tag of server.tags) {
66
+ if (!this.serversByTag.has(tag)) {
67
+ this.serversByTag.set(tag, []);
68
+ }
69
+ this.serversByTag.get(tag).push(server);
70
+ }
44
71
  }
45
72
 
46
73
  markServerAsFailed(serverId) {
47
- const server = this.servers.find(s => s.id === serverId);
74
+ const server = this.serversById.get(serverId);
48
75
  if (server) {
49
76
  server.failedAttempts++;
50
77
  server.healthy = false;
51
-
78
+
79
+ // Invalidar caché porque un servidor cambió de estado
80
+ this.cacheValid = false;
81
+
52
82
  // Programar verificación de salud después de un tiempo
53
83
  setTimeout(() => {
54
84
  this.healthChecker.checkServerHealth(server)
@@ -57,11 +87,17 @@ class ServerPool {
57
87
  server.healthy = true;
58
88
  server.failedAttempts = 0;
59
89
  server.lastChecked = Date.now();
90
+
91
+ // Invalidar caché porque un servidor cambió de estado
92
+ this.cacheValid = false;
60
93
  } else {
61
94
  server.lastChecked = Date.now();
62
95
  // Si ha fallado demasiadas veces, mantenerlo inactivo
63
96
  if (server.failedAttempts > 5) {
64
97
  server.active = false;
98
+
99
+ // Invalidar caché porque un servidor cambió de estado
100
+ this.cacheValid = false;
65
101
  }
66
102
  }
67
103
  });
@@ -70,7 +106,12 @@ class ServerPool {
70
106
  }
71
107
 
72
108
  getServerById(serverId) {
73
- return this.servers.find(server => server.id === serverId);
109
+ return this.serversById.get(serverId);
110
+ }
111
+
112
+ // Método para invalidar la caché manualmente si es necesario
113
+ invalidateCache() {
114
+ this.cacheValid = false;
74
115
  }
75
116
  }
76
117
 
@@ -0,0 +1,90 @@
1
+ const { FilterChain } = require('./FilterChain');
2
+
3
+ /**
4
+ * Extended FilterChain that allows registering filters after startup
5
+ */
6
+ class ExtendedFilterChain {
7
+ constructor() {
8
+ // Create a new instance of the original FilterChain
9
+ this.originalFilterChain = new FilterChain();
10
+
11
+ // Storage for post-startup filters
12
+ this.postStartupFilters = {
13
+ request_processing: [] // Using the same hook type as original
14
+ };
15
+ }
16
+
17
+ /**
18
+ * Register a filter after startup
19
+ * @param {string} type - Type of filter ('request_processing' for now)
20
+ * @param {Function} filterFunction - The filter function to register
21
+ * @param {number} priority - Priority of the filter (lower numbers execute first)
22
+ */
23
+ registerFilter(type, filterFunction, priority = 0) {
24
+ if (!this.postStartupFilters[type]) {
25
+ this.postStartupFilters[type] = [];
26
+ }
27
+
28
+ // Insert filter according to priority
29
+ const filterObj = {
30
+ fn: filterFunction,
31
+ priority,
32
+ id: this.generateId()
33
+ };
34
+
35
+ this.postStartupFilters[type].push(filterObj);
36
+ this.postStartupFilters[type].sort((a, b) => a.priority - b.priority);
37
+
38
+ // Also add to the original hook system
39
+ this.originalFilterChain.addFilter(type, filterFunction);
40
+
41
+ console.log(`Filter registered for ${type} with priority ${priority}`);
42
+ }
43
+
44
+ /**
45
+ * Unregister a specific filter
46
+ * @param {string} type - Type of filter
47
+ * @param {string} filterId - ID of the filter to remove
48
+ */
49
+ unregisterFilter(type, filterId) {
50
+ if (this.postStartupFilters[type]) {
51
+ this.postStartupFilters[type] = this.postStartupFilters[type].filter(f => f.id !== filterId);
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Apply all filters of a specific type
57
+ * @param {string} type - Type of filter
58
+ * @param {Object} req - Request object
59
+ * @param {Object} res - Response object
60
+ * @param {Object} additionalData - Additional data to pass to filters
61
+ * @returns {Object} Result of filter processing
62
+ */
63
+ async applyFilters(type, req, res, additionalData = {}) {
64
+ // Apply filters using the original system which now includes post-startup filters
65
+ return await this.originalFilterChain.applyFilters(type, req, res, additionalData);
66
+ }
67
+
68
+ /**
69
+ * Generate a unique ID
70
+ * @returns {string} Unique identifier
71
+ */
72
+ generateId() {
73
+ return Date.now().toString(36) + Math.random().toString(36).substr(2);
74
+ }
75
+
76
+ // Proxy other methods to the original FilterChain
77
+ addFilter(filterName, filterFunction) {
78
+ this.originalFilterChain.addFilter(filterName, filterFunction);
79
+ }
80
+
81
+ applySpecificFilter(filterName, data) {
82
+ return this.originalFilterChain.applySpecificFilter(filterName, data);
83
+ }
84
+
85
+ getCacheStats() {
86
+ return this.originalFilterChain.getCacheStats();
87
+ }
88
+ }
89
+
90
+ module.exports = { ExtendedFilterChain };
@@ -0,0 +1,87 @@
1
+ const { HookManager } = require('./HookManager');
2
+
3
+ /**
4
+ * Extended HookManager that allows registering hooks after startup
5
+ */
6
+ class ExtendedHookManager {
7
+ constructor() {
8
+ // Create a new instance of the original HookManager
9
+ this.originalHookManager = new HookManager();
10
+
11
+ // Storage for post-startup hooks
12
+ this.postStartupHooks = {};
13
+ }
14
+
15
+ /**
16
+ * Register a hook after startup
17
+ * @param {string} hookName - Name of the hook
18
+ * @param {Function} callback - The callback function to register
19
+ * @param {number} priority - Priority of the hook (lower numbers execute first)
20
+ */
21
+ registerHook(hookName, callback, priority = 0) {
22
+ if (!this.postStartupHooks[hookName]) {
23
+ this.postStartupHooks[hookName] = [];
24
+ }
25
+
26
+ const hookObj = {
27
+ callback,
28
+ priority,
29
+ id: this.generateId()
30
+ };
31
+
32
+ this.postStartupHooks[hookName].push(hookObj);
33
+ this.postStartupHooks[hookName].sort((a, b) => a.priority - b.priority);
34
+
35
+ // Add to the original hook system
36
+ this.originalHookManager.addHook(hookName, callback);
37
+
38
+ console.log(`Hook registered: ${hookName} with priority ${priority}`);
39
+ }
40
+
41
+ /**
42
+ * Unregister a specific hook
43
+ * @param {string} hookName - Name of the hook
44
+ * @param {string} hookId - ID of the hook to remove
45
+ */
46
+ unregisterHook(hookName, hookId) {
47
+ if (this.postStartupHooks[hookName]) {
48
+ this.postStartupHooks[hookName] = this.postStartupHooks[hookName].filter(h => h.id !== hookId);
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Execute all hooks for a specific event
54
+ * @param {string} hookName - Name of the hook
55
+ * @param {Object} data - Data to pass to the hooks
56
+ */
57
+ async executeHooks(hookName, data) {
58
+ // Execute hooks using the original system which now includes post-startup hooks
59
+ return await this.originalHookManager.executeHooks(hookName, data);
60
+ }
61
+
62
+ /**
63
+ * Generate a unique ID
64
+ * @returns {string} Unique identifier
65
+ */
66
+ generateId() {
67
+ return Date.now().toString(36) + Math.random().toString(36).substr(2);
68
+ }
69
+
70
+ // Proxy other methods to the original HookManager
71
+ addHook(hookName, callback) {
72
+ this.originalHookManager.addHook(hookName, callback);
73
+ }
74
+
75
+ executeHooks(hookName, data) {
76
+ return this.originalHookManager.executeHooks(hookName, data);
77
+ }
78
+
79
+ getRegisteredHooks() {
80
+ return Object.keys(this.postStartupHooks).reduce((acc, hookName) => {
81
+ acc[hookName] = this.postStartupHooks[hookName].length;
82
+ return acc;
83
+ }, {});
84
+ }
85
+ }
86
+
87
+ module.exports = { ExtendedHookManager };
@@ -0,0 +1,97 @@
1
+ /**
2
+ * API for registering extensions after Kukuy startup
3
+ */
4
+ class PostStartupExtension {
5
+ constructor(balancer) {
6
+ this.balancer = balancer;
7
+ }
8
+
9
+ /**
10
+ * Register a filter after startup
11
+ * @param {string} type - Type of filter ('request_processing' for now)
12
+ * @param {Function} filterFunction - The filter function to register
13
+ * @param {number} priority - Priority of the filter (lower numbers execute first)
14
+ */
15
+ registerFilter(type, filterFunction, priority = 0) {
16
+ if (this.balancer.filterChain && typeof this.balancer.filterChain.registerFilter === 'function') {
17
+ this.balancer.filterChain.registerFilter(type, filterFunction, priority);
18
+ } else {
19
+ console.error('FilterChain no disponible o registerFilter no es una función');
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Unregister a filter
25
+ * @param {string} type - Type of filter
26
+ * @param {string} filterId - ID of the filter to remove
27
+ */
28
+ unregisterFilter(type, filterId) {
29
+ if (this.balancer.filterChain && typeof this.balancer.filterChain.unregisterFilter === 'function') {
30
+ this.balancer.filterChain.unregisterFilter(type, filterId);
31
+ } else {
32
+ console.error('FilterChain no disponible o unregisterFilter no es una función');
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Register a hook after startup
38
+ * @param {string} hookName - Name of the hook
39
+ * @param {Function} callback - The callback function to register
40
+ * @param {number} priority - Priority of the hook (lower numbers execute first)
41
+ */
42
+ registerHook(hookName, callback, priority = 0) {
43
+ if (this.balancer.hookManager && typeof this.balancer.hookManager.registerHook === 'function') {
44
+ this.balancer.hookManager.registerHook(hookName, callback, priority);
45
+ } else {
46
+ console.error('HookManager no disponible o registerHook no es una función');
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Unregister a hook
52
+ * @param {string} hookName - Name of the hook
53
+ * @param {string} hookId - ID of the hook to remove
54
+ */
55
+ unregisterHook(hookName, hookId) {
56
+ if (this.balancer.hookManager && typeof this.balancer.hookManager.unregisterHook === 'function') {
57
+ this.balancer.hookManager.unregisterHook(hookName, hookId);
58
+ } else {
59
+ console.error('HookManager no disponible o unregisterHook no es una función');
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Get access to system components
65
+ * @param {string} componentName - Name of the component to access
66
+ * @returns {Object} The requested component or null
67
+ */
68
+ getComponent(componentName) {
69
+ switch(componentName) {
70
+ case 'serverPool':
71
+ return this.balancer.serverPool;
72
+ case 'algorithmManager':
73
+ return this.balancer.algorithmManager;
74
+ case 'metricsCollector':
75
+ return this.balancer.metricsCollector;
76
+ case 'config':
77
+ return this.balancer.config;
78
+ case 'routeLoader':
79
+ return this.balancer.routeLoader;
80
+ default:
81
+ return null;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Get information about registered hooks
87
+ * @returns {Object} Information about registered hooks
88
+ */
89
+ getRegisteredHooks() {
90
+ if (this.balancer.hookManager && typeof this.balancer.hookManager.getRegisteredHooks === 'function') {
91
+ return this.balancer.hookManager.getRegisteredHooks();
92
+ }
93
+ return {};
94
+ }
95
+ }
96
+
97
+ module.exports = { PostStartupExtension };
@@ -0,0 +1,183 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * PluginInstance represents a loaded plugin instance
6
+ */
7
+ class PluginInstance {
8
+ constructor(name, manifest, module, balancer) {
9
+ this.name = name;
10
+ this.manifest = manifest;
11
+ this.module = module;
12
+ this.balancer = balancer;
13
+ this.isActive = false;
14
+ }
15
+
16
+ async activate() {
17
+ if (typeof this.module.init === 'function') {
18
+ await this.module.init(this.balancer);
19
+ }
20
+ this.isActive = true;
21
+ }
22
+
23
+ async deactivate() {
24
+ if (typeof this.module.deinit === 'function') {
25
+ await this.module.deinit(this.balancer);
26
+ }
27
+ this.isActive = false;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * PluginManager handles the discovery and loading of plugins
33
+ */
34
+ class PluginManager {
35
+ constructor(balancer) {
36
+ this.balancer = balancer;
37
+ this.plugins = new Map();
38
+ this.pluginPath = process.env.PLUGINS_DIR || './kukuy-plugins';
39
+ }
40
+
41
+ /**
42
+ * Check if a path is a directory
43
+ * @param {string} filePath - Path to check
44
+ * @returns {boolean} - True if path is a directory
45
+ */
46
+ isDirectory(filePath) {
47
+ try {
48
+ const stat = fs.statSync(filePath);
49
+ return stat.isDirectory();
50
+ } catch (error) {
51
+ return false;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Load all plugins from the plugins directory
57
+ */
58
+ async loadPlugins() {
59
+ if (!fs.existsSync(this.pluginPath)) {
60
+ console.log(`Plugin directory not found: ${this.pluginPath}`);
61
+ return;
62
+ }
63
+
64
+ const pluginDirs = fs.readdirSync(this.pluginPath)
65
+ .filter(item => this.isDirectory(path.join(this.pluginPath, item)));
66
+
67
+ console.log(`Found ${pluginDirs.length} potential plugins in ${this.pluginPath}`);
68
+
69
+ for (const pluginDir of pluginDirs) {
70
+ await this.loadPlugin(pluginDir);
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Load a single plugin
76
+ * @param {string} pluginDir - Name of the plugin directory
77
+ */
78
+ async loadPlugin(pluginDir) {
79
+ const pluginPath = path.join(this.pluginPath, pluginDir);
80
+ const manifestPath = path.join(pluginPath, 'manifest.json');
81
+
82
+ if (!fs.existsSync(manifestPath)) {
83
+ console.warn(`Plugin ${pluginDir} does not have manifest.json, skipping...`);
84
+ return;
85
+ }
86
+
87
+ try {
88
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
89
+
90
+ if (!this.validateManifest(manifest)) {
91
+ console.error(`Invalid manifest for plugin ${pluginDir}`);
92
+ return;
93
+ }
94
+
95
+ // Check Kukuy version compatibility
96
+ if (!this.isCompatibleVersion(manifest.kukuyVersion)) {
97
+ console.error(`Plugin ${pluginDir} requires incompatible Kukuy version: ${manifest.kukuyVersion}`);
98
+ return;
99
+ }
100
+
101
+ if (!manifest.enabled) {
102
+ console.log(`Plugin ${pluginDir} is disabled`);
103
+ return;
104
+ }
105
+
106
+ const mainPath = path.join(pluginPath, manifest.main || 'index.js');
107
+ if (!fs.existsSync(mainPath)) {
108
+ console.error(`Main file not found for plugin ${pluginDir}: ${mainPath}`);
109
+ return;
110
+ }
111
+
112
+ // Resolve the path to avoid conflicts with other requires
113
+ const pluginModule = require(path.resolve(mainPath));
114
+ const pluginInstance = new PluginInstance(pluginDir, manifest, pluginModule, this.balancer);
115
+
116
+ this.plugins.set(pluginDir, pluginInstance);
117
+ await pluginInstance.activate();
118
+
119
+ console.log(`Plugin ${pluginDir} loaded successfully`);
120
+ } catch (error) {
121
+ console.error(`Error loading plugin ${pluginDir}:`, error);
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Validate the plugin manifest
127
+ * @param {Object} manifest - The manifest object to validate
128
+ * @returns {boolean} - True if manifest is valid
129
+ */
130
+ validateManifest(manifest) {
131
+ const requiredFields = ['name', 'version', 'description', 'main'];
132
+ const isValid = requiredFields.every(field => manifest.hasOwnProperty(field));
133
+
134
+ if (!isValid) {
135
+ console.error('Missing required fields in manifest:', requiredFields.filter(field => !manifest.hasOwnProperty(field)));
136
+ }
137
+
138
+ return isValid;
139
+ }
140
+
141
+ /**
142
+ * Check if the plugin is compatible with current Kukuy version
143
+ * @param {string} requiredVersion - Version requirement from manifest
144
+ * @returns {boolean} - True if compatible
145
+ */
146
+ isCompatibleVersion(requiredVersion) {
147
+ if (!requiredVersion) {
148
+ // If no version specified, assume compatibility
149
+ return true;
150
+ }
151
+
152
+ // Simple version check - in a real implementation, you'd want to use semver
153
+ const currentVersion = require('../../package.json').version;
154
+
155
+ // For now, just check if the major version matches or if it's a compatible range
156
+ if (requiredVersion.startsWith('^')) {
157
+ const requiredMajor = requiredVersion.slice(1).split('.')[0];
158
+ const currentMajor = currentVersion.split('.')[0];
159
+ return currentMajor === requiredMajor;
160
+ } else if (requiredVersion.startsWith('~')) {
161
+ const [reqMajor, reqMinor] = requiredVersion.slice(1).split('.');
162
+ const [curMajor, curMinor] = currentVersion.split('.');
163
+ return currentMajor === reqMajor && currentMinor === reqMinor;
164
+ } else {
165
+ return currentVersion === requiredVersion;
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Get information about loaded plugins
171
+ * @returns {Array} - Array of plugin information
172
+ */
173
+ getLoadedPlugins() {
174
+ return Array.from(this.plugins.values()).map(plugin => ({
175
+ name: plugin.name,
176
+ version: plugin.manifest.version,
177
+ description: plugin.manifest.description,
178
+ isActive: plugin.isActive
179
+ }));
180
+ }
181
+ }
182
+
183
+ module.exports = { PluginManager };
@@ -15,34 +15,40 @@ class HealthChecker {
15
15
  port: parsedUrl.port,
16
16
  path: '/health', // Ruta estándar para verificación de salud
17
17
  method: 'GET',
18
- timeout: this.timeout
18
+ timeout: this.timeout,
19
+ // Agregar headers para identificar la solicitud de health check
20
+ headers: {
21
+ 'User-Agent': 'Kukuy-Health-Check/1.0'
22
+ }
19
23
  };
20
24
 
21
25
  return new Promise((resolve) => {
22
- const request = server.protocol === 'https:'
26
+ const request = server.protocol === 'https:'
23
27
  ? https.request(options)
24
28
  : http.request(options);
25
29
 
26
30
  request.on('response', (res) => {
31
+ // Consumir el cuerpo de la respuesta para liberar recursos
32
+ res.resume();
33
+
27
34
  // Considerar saludable si obtenemos una respuesta exitosa
28
35
  const isHealthy = res.statusCode >= 200 && res.statusCode < 400;
29
36
  resolve(isHealthy);
30
37
  });
31
38
 
32
39
  request.on('error', (err) => {
33
- console.error(`Error verificando salud de ${server.url}: ${err.message}`);
40
+ // No imprimir errores de health check en consola para evitar spam
34
41
  resolve(false);
35
42
  });
36
43
 
37
44
  request.on('timeout', () => {
38
- console.error(`Timeout verificando salud de ${server.url}`);
45
+ // No imprimir errores de timeout de health check en consola
39
46
  resolve(false);
40
47
  });
41
48
 
42
49
  request.end();
43
50
  });
44
51
  } catch (error) {
45
- console.error(`Error en la verificación de salud: ${error.message}`);
46
52
  return false;
47
53
  }
48
54
  }