versacompiler 2.3.5 → 2.4.1
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.
- package/README.md +1 -1
- package/dist/compiler/compile.js +205 -205
- package/dist/compiler/integrity-validator.js +9 -11
- package/dist/compiler/minify.js +4 -5
- package/dist/compiler/transforms.js +1 -1
- package/dist/compiler/typescript-error-parser.js +0 -133
- package/dist/compiler/typescript-sync-validator.js +12 -7
- package/dist/compiler/typescript-worker-pool.js +153 -34
- package/dist/compiler/typescript-worker-thread.cjs +68 -59
- package/dist/compiler/vuejs.js +12 -6
- package/dist/hrm/initHRM.js +2 -2
- package/dist/main.js +4 -4
- package/dist/servicios/file-watcher.js +3 -1
- package/dist/servicios/readConfig.js +11 -1
- package/package.json +30 -30
|
@@ -15,6 +15,7 @@ export class IntegrityValidator {
|
|
|
15
15
|
static instance;
|
|
16
16
|
cache = new Map();
|
|
17
17
|
MAX_CACHE_SIZE = 100;
|
|
18
|
+
CACHE_TTL = 30 * 60 * 1000; // 30 minutos
|
|
18
19
|
// Estadísticas
|
|
19
20
|
stats = {
|
|
20
21
|
totalValidations: 0,
|
|
@@ -47,12 +48,12 @@ export class IntegrityValidator {
|
|
|
47
48
|
// Revisar cache
|
|
48
49
|
const cacheKey = this.getCacheKey(context, processed);
|
|
49
50
|
const cached = this.cache.get(cacheKey);
|
|
50
|
-
if (cached) {
|
|
51
|
+
if (cached && (Date.now() - cached.timestamp) < this.CACHE_TTL) {
|
|
51
52
|
this.stats.cacheHits++;
|
|
52
53
|
if (options.verbose) {
|
|
53
54
|
logger.info(`[IntegrityValidator] Cache hit for ${context}`);
|
|
54
55
|
}
|
|
55
|
-
return cached;
|
|
56
|
+
return cached.result;
|
|
56
57
|
}
|
|
57
58
|
this.stats.cacheMisses++;
|
|
58
59
|
const errors = [];
|
|
@@ -70,14 +71,11 @@ export class IntegrityValidator {
|
|
|
70
71
|
this.handleValidationResult(result, context, options);
|
|
71
72
|
return result;
|
|
72
73
|
}
|
|
73
|
-
// Check 2: Structure (~1ms)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
// 'Estructura de código inválida (paréntesis/llaves/corchetes desbalanceados)',
|
|
79
|
-
// );
|
|
80
|
-
// }
|
|
74
|
+
// Check 2: Structure (~1ms)
|
|
75
|
+
const structureOk = this.checkStructure(processed);
|
|
76
|
+
if (!structureOk) {
|
|
77
|
+
errors.push('Estructura de código inválida (paréntesis/llaves/corchetes desbalanceados)');
|
|
78
|
+
}
|
|
81
79
|
// Check 3: Exports (~1ms)
|
|
82
80
|
const exportsOk = this.checkExports(original, processed);
|
|
83
81
|
if (!exportsOk) {
|
|
@@ -493,7 +491,7 @@ export class IntegrityValidator {
|
|
|
493
491
|
this.cache.delete(firstKey);
|
|
494
492
|
}
|
|
495
493
|
}
|
|
496
|
-
this.cache.set(key, result);
|
|
494
|
+
this.cache.set(key, { result, timestamp: Date.now() });
|
|
497
495
|
}
|
|
498
496
|
/**
|
|
499
497
|
* Obtener estadísticas de validación
|
package/dist/compiler/minify.js
CHANGED
|
@@ -57,13 +57,12 @@ class MinificationCache {
|
|
|
57
57
|
const validation = integrityValidator.validate(data, result.code, `minify:${filename}`, {
|
|
58
58
|
skipSyntaxCheck: false,
|
|
59
59
|
verbose: process.env.VERBOSE === 'true',
|
|
60
|
-
throwOnError:
|
|
60
|
+
throwOnError: false, // Usar fallback en lugar de lanzar excepción
|
|
61
61
|
});
|
|
62
62
|
if (!validation.valid) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
throw new Error(`Integrity check failed for ${filename}: ${validation.errors.join(', ')}`);
|
|
63
|
+
logger.warn(`⚠️ Validación de integridad fallida para ${filename}: ${validation.errors.join(', ')}`);
|
|
64
|
+
logger.warn(` Usando código original sin minificar`);
|
|
65
|
+
return { code: data, error: null, cached: false };
|
|
67
66
|
}
|
|
68
67
|
if (process.env.VERBOSE === 'true') {
|
|
69
68
|
logger.info(`✅ Validación de integridad OK para ${filename} (${validation.metrics.duration.toFixed(2)}ms)`);
|
|
@@ -3,8 +3,8 @@ import { env } from 'node:process';
|
|
|
3
3
|
import { logger } from '../servicios/logger.js';
|
|
4
4
|
import { EXCLUDED_MODULES } from '../utils/excluded-modules.js';
|
|
5
5
|
import { getModuleSubPath } from '../utils/module-resolver.js';
|
|
6
|
-
import { integrityValidator } from './integrity-validator.js';
|
|
7
6
|
import { analyzeAndFormatMultipleErrors } from './error-reporter.js';
|
|
7
|
+
import { integrityValidator } from './integrity-validator.js';
|
|
8
8
|
import { getOptimizedAliasPath, getOptimizedModulePath, } from './module-resolution-optimizer.js';
|
|
9
9
|
import { parser } from './parser.js';
|
|
10
10
|
// ✨ OPTIMIZACIÓN CRÍTICA: Cache de PATH_ALIAS parseado
|
|
@@ -135,139 +135,6 @@ function cleanErrorMessage(message) {
|
|
|
135
135
|
.replace(/\s+/g, ' ')
|
|
136
136
|
.trim());
|
|
137
137
|
}
|
|
138
|
-
/**
|
|
139
|
-
* Mejora significativamente el mensaje de error TypeScript con contexto visual
|
|
140
|
-
* ⚠️ DEPRECATED: Ya no se usa en el flujo normal para evitar overhead de performance
|
|
141
|
-
* Se mantiene para compatibilidad futura o modo verbose avanzado
|
|
142
|
-
* @param scriptInfo - Información de extracción de script para archivos Vue (opcional)
|
|
143
|
-
*/
|
|
144
|
-
function enhanceErrorMessage(diagnostic, fileName, sourceCode, scriptInfo) {
|
|
145
|
-
// Extraer el mensaje del error
|
|
146
|
-
const message = typeof diagnostic.messageText === 'string'
|
|
147
|
-
? diagnostic.messageText
|
|
148
|
-
: typescript.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
|
|
149
|
-
const enhancedMessage = cleanErrorMessage(message); // Información de ubicación
|
|
150
|
-
let location = `Código TS${diagnostic.code}`;
|
|
151
|
-
let codeContext = '';
|
|
152
|
-
if (diagnostic.file && diagnostic.start !== undefined) {
|
|
153
|
-
const sourceFile = diagnostic.file;
|
|
154
|
-
// Verificar que el método getLineAndCharacterOfPosition existe (para compatibilidad con mocks)
|
|
155
|
-
if (typeof sourceFile.getLineAndCharacterOfPosition === 'function') {
|
|
156
|
-
try {
|
|
157
|
-
const lineAndChar = sourceFile.getLineAndCharacterOfPosition(diagnostic.start);
|
|
158
|
-
// Ajustar línea si es un archivo Vue con script extraído
|
|
159
|
-
const line = scriptInfo
|
|
160
|
-
? lineAndChar.line + scriptInfo.startLine
|
|
161
|
-
: lineAndChar.line + 1;
|
|
162
|
-
const column = lineAndChar.character + 1;
|
|
163
|
-
location = `Línea ${line}, Columna ${column} | Código TS${diagnostic.code}`;
|
|
164
|
-
}
|
|
165
|
-
catch {
|
|
166
|
-
// Si falla, solo mostrar el código de error
|
|
167
|
-
location = `Código TS${diagnostic.code}`;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
else {
|
|
171
|
-
// Fallback: solo mostrar el código de error
|
|
172
|
-
location = `Código TS${diagnostic.code}`;
|
|
173
|
-
} // Agregar contexto del código si está disponible
|
|
174
|
-
if ((sourceCode || scriptInfo?.originalData || sourceFile.text) &&
|
|
175
|
-
typeof sourceFile.getLineAndCharacterOfPosition === 'function') {
|
|
176
|
-
try {
|
|
177
|
-
const lineAndChar = sourceFile.getLineAndCharacterOfPosition(diagnostic.start);
|
|
178
|
-
// Obtener código fuente apropiado
|
|
179
|
-
const text = scriptInfo?.originalData || sourceCode || sourceFile.text;
|
|
180
|
-
const lines = text.split('\n');
|
|
181
|
-
// Calcular la línea real en el archivo original
|
|
182
|
-
const actualLineIndex = scriptInfo
|
|
183
|
-
? lineAndChar.line + scriptInfo.startLine - 1
|
|
184
|
-
: lineAndChar.line;
|
|
185
|
-
const errorLine = lines[actualLineIndex];
|
|
186
|
-
if (errorLine) {
|
|
187
|
-
// Mostrar hasta 2 líneas antes y después para contexto
|
|
188
|
-
const startLine = Math.max(0, actualLineIndex - 2);
|
|
189
|
-
const endLine = Math.min(lines.length - 1, actualLineIndex + 2);
|
|
190
|
-
codeContext = '\n\n📝 Contexto del código:\n';
|
|
191
|
-
for (let i = startLine; i <= endLine; i++) {
|
|
192
|
-
const currentLine = i + 1;
|
|
193
|
-
const lineContent = lines[i] || '';
|
|
194
|
-
const isErrorLine = i === actualLineIndex;
|
|
195
|
-
if (isErrorLine) {
|
|
196
|
-
codeContext += ` ${currentLine.toString().padStart(3, ' ')} ❌ ${lineContent}\n`;
|
|
197
|
-
// Agregar flecha apuntando al error
|
|
198
|
-
const arrow = ' '.repeat(6 + lineAndChar.character + 1) +
|
|
199
|
-
'^^^';
|
|
200
|
-
codeContext += ` ${arrow}\n`;
|
|
201
|
-
}
|
|
202
|
-
else {
|
|
203
|
-
codeContext += ` ${currentLine.toString().padStart(3, ' ')} ${lineContent}\n`;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
catch {
|
|
209
|
-
// Si falla obtener el contexto, continuar sin él
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
// Agregar sugerencias basadas en el tipo de error
|
|
214
|
-
const suggestions = getErrorSuggestions(diagnostic.code, enhancedMessage);
|
|
215
|
-
const suggestionsText = suggestions.length > 0
|
|
216
|
-
? `\n\n💡 Sugerencias:\n${suggestions.map(s => ` • ${s}`).join('\n')}`
|
|
217
|
-
: '';
|
|
218
|
-
return `${enhancedMessage}\n 📍 ${location}${codeContext}${suggestionsText}`;
|
|
219
|
-
}
|
|
220
|
-
/**
|
|
221
|
-
* Proporciona sugerencias específicas basadas en el código de error TypeScript
|
|
222
|
-
*/
|
|
223
|
-
function getErrorSuggestions(errorCode, message) {
|
|
224
|
-
const suggestions = [];
|
|
225
|
-
switch (errorCode) {
|
|
226
|
-
case 2304: // Cannot find name
|
|
227
|
-
suggestions.push('Verifica que la variable esté declarada');
|
|
228
|
-
suggestions.push('Asegúrate de importar el módulo correspondiente');
|
|
229
|
-
suggestions.push('Revisa la ortografía del nombre');
|
|
230
|
-
break;
|
|
231
|
-
case 2322: // Type assignment error
|
|
232
|
-
suggestions.push('Verifica que los tipos sean compatibles');
|
|
233
|
-
suggestions.push('Considera usar type assertion: valor as TipoEsperado');
|
|
234
|
-
break;
|
|
235
|
-
case 2307: // Cannot find module
|
|
236
|
-
suggestions.push('Verifica que el archivo exista en la ruta especificada');
|
|
237
|
-
suggestions.push('Revisa las rutas en tsconfig.json');
|
|
238
|
-
suggestions.push('Asegúrate de que el paquete esté instalado');
|
|
239
|
-
break;
|
|
240
|
-
case 2451: // Cannot redeclare block-scoped variable
|
|
241
|
-
suggestions.push('Cambia el nombre de la variable');
|
|
242
|
-
suggestions.push('Usa un scope diferente (function, block)');
|
|
243
|
-
break;
|
|
244
|
-
case 7006: // Parameter implicitly has 'any' type
|
|
245
|
-
suggestions.push('Agrega tipos explícitos a los parámetros');
|
|
246
|
-
suggestions.push('Considera habilitar "noImplicitAny": false en tsconfig.json');
|
|
247
|
-
break;
|
|
248
|
-
case 1155: // 'const' declarations must be initialized
|
|
249
|
-
suggestions.push('Agrega un valor inicial: const variable = valor;');
|
|
250
|
-
suggestions.push('O cambia a "let" si quieres asignar después');
|
|
251
|
-
break;
|
|
252
|
-
case 2339: // Property does not exist
|
|
253
|
-
suggestions.push('Verifica que la propiedad exista en el tipo');
|
|
254
|
-
suggestions.push('Considera usar optional chaining: objeto?.propiedad');
|
|
255
|
-
break;
|
|
256
|
-
default:
|
|
257
|
-
// Sugerencias genéricas basadas en el mensaje
|
|
258
|
-
if (message.includes('Cannot find')) {
|
|
259
|
-
suggestions.push('Verifica que el elemento exista y esté importado');
|
|
260
|
-
}
|
|
261
|
-
if (message.includes('Type')) {
|
|
262
|
-
suggestions.push('Revisa la compatibilidad de tipos');
|
|
263
|
-
}
|
|
264
|
-
if (message.includes('missing')) {
|
|
265
|
-
suggestions.push('Agrega el elemento faltante');
|
|
266
|
-
}
|
|
267
|
-
break;
|
|
268
|
-
}
|
|
269
|
-
return suggestions;
|
|
270
|
-
}
|
|
271
138
|
/**
|
|
272
139
|
* Crea un mensaje de error unificado para errores múltiples
|
|
273
140
|
*/
|
|
@@ -94,17 +94,17 @@ class TypeScriptLanguageServiceHost {
|
|
|
94
94
|
getDefaultLibFileName(options) {
|
|
95
95
|
return typescript.getDefaultLibFilePath(options);
|
|
96
96
|
}
|
|
97
|
-
fileExists(
|
|
98
|
-
return this.files.has(
|
|
97
|
+
fileExists(filePath) {
|
|
98
|
+
return this.files.has(filePath) || fs.existsSync(filePath);
|
|
99
99
|
}
|
|
100
|
-
readFile(
|
|
101
|
-
const file = this.files.get(
|
|
100
|
+
readFile(filePath) {
|
|
101
|
+
const file = this.files.get(filePath);
|
|
102
102
|
if (file) {
|
|
103
103
|
return file.content;
|
|
104
104
|
}
|
|
105
|
-
if (fs.existsSync(
|
|
105
|
+
if (fs.existsSync(filePath)) {
|
|
106
106
|
try {
|
|
107
|
-
return fs.readFileSync(
|
|
107
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
108
108
|
}
|
|
109
109
|
catch {
|
|
110
110
|
return undefined;
|
|
@@ -167,7 +167,6 @@ export const validateTypesWithLanguageService = (fileName, content, compilerOpti
|
|
|
167
167
|
try {
|
|
168
168
|
// Verificar que el archivo existe en el host antes de solicitar diagnósticos
|
|
169
169
|
if (!host.fileExists(actualFileName)) {
|
|
170
|
-
console.log('File does not exist in host, returning empty result');
|
|
171
170
|
return { diagnostics: [], hasErrors: false };
|
|
172
171
|
} // Obtener diagnósticos de tipos con manejo de errores
|
|
173
172
|
let syntacticDiagnostics = [];
|
|
@@ -266,6 +265,12 @@ export const validateTypesWithLanguageService = (fileName, content, compilerOpti
|
|
|
266
265
|
catch {
|
|
267
266
|
return { diagnostics: [], hasErrors: false };
|
|
268
267
|
}
|
|
268
|
+
finally {
|
|
269
|
+
try {
|
|
270
|
+
languageService.dispose();
|
|
271
|
+
}
|
|
272
|
+
catch { /* ignore dispose errors */ }
|
|
273
|
+
}
|
|
269
274
|
}
|
|
270
275
|
catch (error) {
|
|
271
276
|
// En caso de error, devolver diagnóstico de error
|
|
@@ -19,22 +19,27 @@ export class TypeScriptWorkerPool {
|
|
|
19
19
|
workerPath;
|
|
20
20
|
initPromise = null;
|
|
21
21
|
isInitialized = false; // Configuración optimizada con reciclaje de workers
|
|
22
|
-
TASK_TIMEOUT =
|
|
23
|
-
WORKER_INIT_TIMEOUT =
|
|
22
|
+
TASK_TIMEOUT = 60000; // 60 segundos por tarea - workers son secuenciales, necesitan tiempo
|
|
23
|
+
WORKER_INIT_TIMEOUT = 10000; // 10 segundos para inicializar
|
|
24
24
|
MAX_TASKS_PER_WORKER = 200; // ✨ OPTIMIZADO: Aumentado de 50 a 200 para reducir overhead de reciclaje
|
|
25
25
|
WORKER_MEMORY_CHECK_INTERVAL = 500; // ✨ OPTIMIZADO: Verificar cada 500 tareas (reducir overhead)
|
|
26
26
|
// ✨ FIX #1: Referencias a timers para limpieza adecuada
|
|
27
27
|
memoryCheckInterval = null;
|
|
28
28
|
cleanupInterval = null;
|
|
29
|
+
// Cola de tareas para cuando todos los workers están ocupados
|
|
30
|
+
taskQueue = [];
|
|
31
|
+
MAX_QUEUE_SIZE = 1000; // Suficiente para proyectos de 300+ archivos
|
|
32
|
+
// Flag para evitar concurrencia en ensureWorkerCapacity
|
|
33
|
+
isScalingUp = false;
|
|
29
34
|
// Métricas de rendimiento
|
|
30
35
|
totalTasks = 0;
|
|
31
36
|
completedTasks = 0;
|
|
32
37
|
failedTasks = 0;
|
|
33
38
|
constructor() {
|
|
34
|
-
// Determinar tamaño
|
|
39
|
+
// Determinar tamaño conservador del pool para evitar sobrecarga de RAM/CPU
|
|
35
40
|
const cpuCount = os.cpus().length;
|
|
36
|
-
|
|
37
|
-
this.poolSize = Math.min(Math.max(
|
|
41
|
+
const configuredMax = parseInt(process.env.TS_MAX_WORKERS || '2', 10);
|
|
42
|
+
this.poolSize = Math.min(configuredMax, Math.max(1, cpuCount - 1));
|
|
38
43
|
this.workerPath = path.join(process.env.PATH_PROY || path.join(process.cwd(), 'src'), 'compiler', 'typescript-worker-thread.cjs');
|
|
39
44
|
// ✨ ISSUE #4: Configurar monitoreo de memoria automático this.startMemoryMonitoring();
|
|
40
45
|
}
|
|
@@ -80,8 +85,11 @@ export class TypeScriptWorkerPool {
|
|
|
80
85
|
// ✨ ISSUE #4: Obtener memoria real del worker
|
|
81
86
|
const memoryInfo = await this.getWorkerMemoryUsage(poolWorker);
|
|
82
87
|
poolWorker.memoryUsage = memoryInfo.heapUsed;
|
|
83
|
-
// Verificar límites de memoria y reciclaje automático
|
|
84
|
-
|
|
88
|
+
// Verificar límites de memoria y reciclaje automático.
|
|
89
|
+
// Solo reciclar workers INACTIVOS para evitar rechazar tareas en vuelo.
|
|
90
|
+
// Los workers con tareas pendientes se reciclan vía drainQueue cuando quedan libres.
|
|
91
|
+
const isIdle = !poolWorker.busy && poolWorker.pendingTasks.size === 0;
|
|
92
|
+
if (isIdle && this.shouldRecycleWorker(poolWorker)) {
|
|
85
93
|
const reason = this.getRecycleReason(poolWorker);
|
|
86
94
|
console.warn(`[WorkerPool] Worker ${poolWorker.id} requiere reciclaje: ${reason}`);
|
|
87
95
|
await this.recycleWorker(poolWorker);
|
|
@@ -190,19 +198,19 @@ export class TypeScriptWorkerPool {
|
|
|
190
198
|
* Configura el modo de operación del pool - OPTIMIZADO para máxima velocidad
|
|
191
199
|
*/
|
|
192
200
|
setMode(mode) {
|
|
193
|
-
|
|
201
|
+
const cpuCount = os.cpus().length;
|
|
194
202
|
switch (mode) {
|
|
195
203
|
case 'batch':
|
|
196
|
-
// Para modo batch,
|
|
197
|
-
this.poolSize = Math.min(
|
|
204
|
+
// Para modo batch, hasta 4 workers para mejor rendimiento en proyectos grandes
|
|
205
|
+
this.poolSize = Math.min(4, Math.max(1, cpuCount - 1));
|
|
198
206
|
break;
|
|
199
207
|
case 'watch':
|
|
200
|
-
// Para modo watch,
|
|
201
|
-
this.poolSize = Math.min(
|
|
208
|
+
// Para modo watch, hasta 2 workers para menor impacto en sistema
|
|
209
|
+
this.poolSize = Math.min(2, Math.max(1, cpuCount - 1));
|
|
202
210
|
break;
|
|
203
211
|
case 'individual':
|
|
204
|
-
// Para individual,
|
|
205
|
-
this.poolSize =
|
|
212
|
+
// Para individual, 1 worker suficiente
|
|
213
|
+
this.poolSize = 1;
|
|
206
214
|
break;
|
|
207
215
|
}
|
|
208
216
|
}
|
|
@@ -227,9 +235,9 @@ export class TypeScriptWorkerPool {
|
|
|
227
235
|
if (!exists) {
|
|
228
236
|
throw new Error(`Worker thread file not found: ${this.workerPath}`);
|
|
229
237
|
}
|
|
230
|
-
// Crear
|
|
231
|
-
const
|
|
232
|
-
this.workers =
|
|
238
|
+
// Crear solo el primer worker al iniciar; el resto se crean bajo demanda
|
|
239
|
+
const firstWorker = await this.createWorker(0);
|
|
240
|
+
this.workers = [firstWorker];
|
|
233
241
|
this.isInitialized = true;
|
|
234
242
|
}
|
|
235
243
|
catch (error) {
|
|
@@ -237,6 +245,30 @@ export class TypeScriptWorkerPool {
|
|
|
237
245
|
throw error;
|
|
238
246
|
}
|
|
239
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Crea un worker adicional si todos están ocupados y la cola tiene tareas pendientes.
|
|
250
|
+
* Usa un flag para evitar que múltiples llamadas concurrentes creen workers de más.
|
|
251
|
+
*/
|
|
252
|
+
async ensureWorkerCapacity() {
|
|
253
|
+
if (this.isScalingUp)
|
|
254
|
+
return;
|
|
255
|
+
if (this.workers.length >= this.poolSize)
|
|
256
|
+
return;
|
|
257
|
+
const allBusy = this.workers.every(w => w.busy || w.pendingTasks.size > 0);
|
|
258
|
+
if (!allBusy || this.taskQueue.length === 0)
|
|
259
|
+
return;
|
|
260
|
+
this.isScalingUp = true;
|
|
261
|
+
try {
|
|
262
|
+
const newWorker = await this.createWorker(this.workers.length);
|
|
263
|
+
this.workers.push(newWorker);
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
// Silencioso: se usará la cola cuando un worker quede libre
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
this.isScalingUp = false;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
240
272
|
/**
|
|
241
273
|
* Crea un worker individual
|
|
242
274
|
*/
|
|
@@ -338,17 +370,19 @@ export class TypeScriptWorkerPool {
|
|
|
338
370
|
};
|
|
339
371
|
// ✨ FIX MEMORIA: No mantener response completo en memoria
|
|
340
372
|
resolve(result);
|
|
341
|
-
//
|
|
373
|
+
// Procesar siguiente tarea de la cola y limpiar diagnostics
|
|
342
374
|
setImmediate(() => {
|
|
375
|
+
this.drainQueue(poolWorker);
|
|
343
376
|
if (response.diagnostics) {
|
|
344
377
|
response.diagnostics.length = 0;
|
|
345
378
|
}
|
|
346
379
|
});
|
|
347
380
|
}
|
|
348
381
|
else {
|
|
349
|
-
this.failedTasks++;
|
|
350
382
|
const errorMessage = response.error || 'Error desconocido del worker';
|
|
351
383
|
// ✨ FIX MEMORIA: Error simple sin referencias pesadas
|
|
384
|
+
// failedTasks se incrementa en el wrapper de queued.reject para tareas de cola,
|
|
385
|
+
// o en handleWorkerError/Exit para tareas directas que se re-encolan.
|
|
352
386
|
const error = new Error(errorMessage);
|
|
353
387
|
reject(error);
|
|
354
388
|
}
|
|
@@ -374,7 +408,8 @@ export class TypeScriptWorkerPool {
|
|
|
374
408
|
const pendingTasksArray = Array.from(poolWorker.pendingTasks.entries());
|
|
375
409
|
for (const [taskId, task] of pendingTasksArray) {
|
|
376
410
|
clearTimeout(task.timeout);
|
|
377
|
-
|
|
411
|
+
// failedTasks se contabiliza en el wrapper de queued.reject (para cola)
|
|
412
|
+
// o via re-encolado en typeCheck() catch (para tareas directas).
|
|
378
413
|
task.reject(new Error(`Worker ${poolWorker.id} failed`));
|
|
379
414
|
poolWorker.pendingTasks.delete(taskId);
|
|
380
415
|
}
|
|
@@ -406,6 +441,8 @@ export class TypeScriptWorkerPool {
|
|
|
406
441
|
if (index !== -1) {
|
|
407
442
|
this.workers[index] = newWorker;
|
|
408
443
|
}
|
|
444
|
+
// Drenar la cola con el nuevo worker para evitar tareas atascadas
|
|
445
|
+
setImmediate(() => this.drainAllWorkers());
|
|
409
446
|
}
|
|
410
447
|
catch {
|
|
411
448
|
// Silencioso
|
|
@@ -421,7 +458,8 @@ export class TypeScriptWorkerPool {
|
|
|
421
458
|
// 1. Rechazar tareas pendientes con cleanup
|
|
422
459
|
poolWorker.pendingTasks.forEach(task => {
|
|
423
460
|
clearTimeout(task.timeout);
|
|
424
|
-
|
|
461
|
+
// failedTasks se contabiliza en el wrapper de queued.reject (para cola)
|
|
462
|
+
// o via re-encolado en typeCheck() catch (para tareas directas).
|
|
425
463
|
task.reject(new Error(`Worker ${poolWorker.id} exited with code ${code}`));
|
|
426
464
|
});
|
|
427
465
|
poolWorker.pendingTasks.clear();
|
|
@@ -513,6 +551,8 @@ export class TypeScriptWorkerPool {
|
|
|
513
551
|
if (index !== -1) {
|
|
514
552
|
this.workers[index] = newWorker;
|
|
515
553
|
}
|
|
554
|
+
// Drenar la cola con el nuevo worker listo
|
|
555
|
+
setImmediate(() => this.drainQueue(newWorker));
|
|
516
556
|
}
|
|
517
557
|
catch (error) {
|
|
518
558
|
console.error(`[WorkerPool] Error reciclando worker ${poolWorker.id}:`, error);
|
|
@@ -543,6 +583,48 @@ export class TypeScriptWorkerPool {
|
|
|
543
583
|
}
|
|
544
584
|
return null; // Todos los workers están muy ocupados
|
|
545
585
|
}
|
|
586
|
+
/**
|
|
587
|
+
* Drena la cola en todos los workers disponibles
|
|
588
|
+
*/
|
|
589
|
+
drainAllWorkers() {
|
|
590
|
+
for (const worker of this.workers) {
|
|
591
|
+
if (!worker.busy && worker.pendingTasks.size === 0) {
|
|
592
|
+
this.drainQueue(worker);
|
|
593
|
+
if (this.taskQueue.length === 0)
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Procesa la siguiente tarea de la cola en el worker liberado
|
|
600
|
+
*/
|
|
601
|
+
drainQueue(poolWorker) {
|
|
602
|
+
if (this.taskQueue.length === 0)
|
|
603
|
+
return;
|
|
604
|
+
if (poolWorker.busy || poolWorker.pendingTasks.size > 0)
|
|
605
|
+
return;
|
|
606
|
+
// Si el worker alcanzó el límite de tareas, reciclarlo y luego drenar
|
|
607
|
+
if (poolWorker.taskCounter >= this.MAX_TASKS_PER_WORKER) {
|
|
608
|
+
this.recycleWorker(poolWorker)
|
|
609
|
+
.then(() => setImmediate(() => this.drainAllWorkers()))
|
|
610
|
+
.catch(() => { });
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
const queued = this.taskQueue.shift();
|
|
614
|
+
if (!queued)
|
|
615
|
+
return;
|
|
616
|
+
const waitMs = Date.now() - queued.queuedAt;
|
|
617
|
+
if (waitMs > 30000) {
|
|
618
|
+
// queued.reject incluye failedTasks++ en el wrapper (ver typeCheck())
|
|
619
|
+
queued.reject(new Error(`Queue task expired after ${waitMs}ms`));
|
|
620
|
+
// Continuar drenando la cola con la siguiente tarea
|
|
621
|
+
setImmediate(() => this.drainQueue(poolWorker));
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
this.typeCheckWithWorker(poolWorker, queued.fileName, queued.content, queued.compilerOptions)
|
|
625
|
+
.then(queued.resolve)
|
|
626
|
+
.catch(queued.reject); // queued.reject incluye failedTasks++ en el wrapper
|
|
627
|
+
}
|
|
546
628
|
/**
|
|
547
629
|
* Realiza type checking usando el pool de workers
|
|
548
630
|
*/
|
|
@@ -550,31 +632,61 @@ export class TypeScriptWorkerPool {
|
|
|
550
632
|
// Asegurar que el pool esté inicializado
|
|
551
633
|
await this.initializePool();
|
|
552
634
|
if (!this.isInitialized) {
|
|
635
|
+
// Solo usar fallback síncrono si el pool no pudo inicializarse del todo
|
|
553
636
|
return this.typeCheckWithSyncFallback(fileName, content, compilerOptions);
|
|
554
637
|
}
|
|
638
|
+
this.totalTasks++;
|
|
555
639
|
// Buscar worker disponible
|
|
556
640
|
const availableWorker = this.findAvailableWorker();
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
641
|
+
if (availableWorker) {
|
|
642
|
+
try {
|
|
643
|
+
return await this.typeCheckWithWorker(availableWorker, fileName, content, compilerOptions);
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
// Worker falló o llegó al límite de tareas
|
|
647
|
+
// Si alcanzó el límite, disparar reciclaje inmediato
|
|
648
|
+
if (availableWorker.taskCounter >= this.MAX_TASKS_PER_WORKER) {
|
|
649
|
+
this.recycleWorker(availableWorker)
|
|
650
|
+
.then(() => setImmediate(() => this.drainAllWorkers()))
|
|
651
|
+
.catch(() => { });
|
|
652
|
+
}
|
|
653
|
+
}
|
|
564
654
|
}
|
|
565
|
-
|
|
566
|
-
|
|
655
|
+
// Ningún worker disponible: encolar la tarea
|
|
656
|
+
if (this.taskQueue.length >= this.MAX_QUEUE_SIZE) {
|
|
657
|
+
// Cola llena → omitir type check para no bloquear el event loop
|
|
658
|
+
// (con MAX_QUEUE_SIZE=1000 esto solo ocurre en proyectos extremadamente grandes)
|
|
659
|
+
console.warn(`[TypeCheckQueue] Cola llena (${this.MAX_QUEUE_SIZE} tareas). Type check omitido para: ${path.basename(fileName)}`);
|
|
660
|
+
this.completedTasks++; // Contabilizar como completado para mantener métricas consistentes
|
|
661
|
+
return { diagnostics: [], hasErrors: false };
|
|
567
662
|
}
|
|
663
|
+
// Escalar el pool si es posible
|
|
664
|
+
this.ensureWorkerCapacity().catch(() => { });
|
|
665
|
+
return new Promise((resolve, reject) => {
|
|
666
|
+
// Wrapper para reject que garantiza que failedTasks se incrementa exactamente una vez
|
|
667
|
+
// para todas las tareas que pasan por la cola (drainQueue path).
|
|
668
|
+
const wrappedReject = (err) => {
|
|
669
|
+
this.failedTasks++;
|
|
670
|
+
reject(err);
|
|
671
|
+
};
|
|
672
|
+
this.taskQueue.push({
|
|
673
|
+
fileName,
|
|
674
|
+
content,
|
|
675
|
+
compilerOptions,
|
|
676
|
+
resolve,
|
|
677
|
+
reject: wrappedReject,
|
|
678
|
+
queuedAt: Date.now(),
|
|
679
|
+
});
|
|
680
|
+
});
|
|
568
681
|
}
|
|
569
682
|
/**
|
|
570
683
|
* Realiza type checking usando un worker específico con reciclaje automático
|
|
571
684
|
* ✨ FIX MEMORIA: Optimizado para prevenir fugas de memoria
|
|
572
685
|
*/
|
|
573
686
|
async typeCheckWithWorker(poolWorker, fileName, content, compilerOptions) {
|
|
574
|
-
//
|
|
687
|
+
// Si el worker alcanzó su límite de tareas, encolar para reciclaje
|
|
575
688
|
if (poolWorker.taskCounter >= this.MAX_TASKS_PER_WORKER) {
|
|
576
|
-
|
|
577
|
-
return this.typeCheckWithSyncFallback(fileName, content, compilerOptions);
|
|
689
|
+
throw new Error(`Worker ${poolWorker.id} ha alcanzado el límite de tareas`);
|
|
578
690
|
}
|
|
579
691
|
return new Promise((resolve, reject) => {
|
|
580
692
|
const taskId = `task-${poolWorker.id}-${++poolWorker.taskCounter}-${Date.now()}`;
|
|
@@ -593,6 +705,8 @@ export class TypeScriptWorkerPool {
|
|
|
593
705
|
poolWorker.busy = false;
|
|
594
706
|
}
|
|
595
707
|
reject(new Error(`Timeout (${dynamicTimeout}ms) en type checking para ${fileName}`));
|
|
708
|
+
// Drenar la cola cuando se libera la tarea por timeout
|
|
709
|
+
setImmediate(() => this.drainQueue(poolWorker));
|
|
596
710
|
}
|
|
597
711
|
}, dynamicTimeout);
|
|
598
712
|
// ✨ FIX MEMORIA: Wrapper para resolver/rechazar que limpia el timeout
|
|
@@ -697,7 +811,7 @@ export class TypeScriptWorkerPool {
|
|
|
697
811
|
complexityMultiplier = Math.min(complexityMultiplier, 5); // Máximo 5x el timeout base
|
|
698
812
|
complexityMultiplier = Math.max(complexityMultiplier, 0.5); // Mínimo 0.5x el timeout base
|
|
699
813
|
const finalTimeout = Math.round(baseTimeout * complexityMultiplier);
|
|
700
|
-
return Math.min(finalTimeout,
|
|
814
|
+
return Math.min(finalTimeout, 120000); // Máximo absoluto de 120 segundos
|
|
701
815
|
}
|
|
702
816
|
/**
|
|
703
817
|
* Fallback síncrono para type checking
|
|
@@ -725,6 +839,11 @@ export class TypeScriptWorkerPool {
|
|
|
725
839
|
console.log('[WorkerPool] Cerrando pool de workers...');
|
|
726
840
|
// 0. ✨ FIX #1: Detener monitoreo de memoria primero
|
|
727
841
|
this.stopMemoryMonitoring();
|
|
842
|
+
// 0b. Rechazar todas las tareas en cola
|
|
843
|
+
for (const queued of this.taskQueue) {
|
|
844
|
+
queued.reject(new Error('Worker pool cerrado'));
|
|
845
|
+
}
|
|
846
|
+
this.taskQueue = [];
|
|
728
847
|
// 1. Rechazar todas las tareas pendientes con cleanup
|
|
729
848
|
let totalPendingTasks = 0;
|
|
730
849
|
for (const poolWorker of this.workers) {
|