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.
@@ -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) - TEMPORALMENTE DESHABILITADO
74
- // TODO: Mejorar detección de character classes en regex literals
75
- const structureOk = true; // this.checkStructure(processed);
76
- // if (!structureOk) {
77
- // errors.push(
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
@@ -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: true, // Detener build si falla
60
+ throwOnError: false, // Usar fallback en lugar de lanzar excepción
61
61
  });
62
62
  if (!validation.valid) {
63
- // El validator ya lanzó el error si throwOnError=true
64
- // Pero por si acaso, retornamos el código original
65
- logger.error(`❌ Validación de integridad fallida para ${filename}`, validation.errors.join(', '));
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(path) {
98
- return this.files.has(path) || fs.existsSync(path);
97
+ fileExists(filePath) {
98
+ return this.files.has(filePath) || fs.existsSync(filePath);
99
99
  }
100
- readFile(path) {
101
- const file = this.files.get(path);
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(path)) {
105
+ if (fs.existsSync(filePath)) {
106
106
  try {
107
- return fs.readFileSync(path, 'utf-8');
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 = 8000; // 8 segundos por tarea (reducido para mayor velocidad)
23
- WORKER_INIT_TIMEOUT = 3000; // 3 segundos para inicializar (reducido)
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 óptimo del pool - MÁS AGRESIVO para mejor rendimiento
39
+ // Determinar tamaño conservador del pool para evitar sobrecarga de RAM/CPU
35
40
  const cpuCount = os.cpus().length;
36
- // Usar más workers para aprovechar mejor el CPU
37
- this.poolSize = Math.min(Math.max(cpuCount, 4), 16); // Entre 4 y 16 workers
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
- if (this.shouldRecycleWorker(poolWorker)) {
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
- // Ajustar configuración según el modo - MÁS AGRESIVO
201
+ const cpuCount = os.cpus().length;
194
202
  switch (mode) {
195
203
  case 'batch':
196
- // Para modo batch, máxima concurrencia para throughput
197
- this.poolSize = Math.min(os.cpus().length, 20);
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, más workers para mejor responsividad
201
- this.poolSize = Math.min(os.cpus().length, 12);
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, pool moderado
205
- this.poolSize = Math.min(8, os.cpus().length);
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 workers en paralelo
231
- const workerPromises = Array.from({ length: this.poolSize }, (_, index) => this.createWorker(index));
232
- this.workers = await Promise.all(workerPromises);
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
- // FIX MEMORIA: Limpiar diagnostics después de resolver
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
- this.failedTasks++;
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
- this.failedTasks++;
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
- // FIX: Incrementar totalTasks ANTES del try/catch para conteo correcto
558
- this.totalTasks++;
559
- if (!availableWorker) {
560
- return this.typeCheckWithSyncFallback(fileName, content, compilerOptions);
561
- }
562
- try {
563
- return await this.typeCheckWithWorker(availableWorker, fileName, content, compilerOptions);
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
- catch {
566
- return this.typeCheckWithSyncFallback(fileName, content, compilerOptions);
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
- // Verificar si el worker necesita reciclaje por número de tareas
687
+ // Si el worker alcanzó su límite de tareas, encolar para reciclaje
575
688
  if (poolWorker.taskCounter >= this.MAX_TASKS_PER_WORKER) {
576
- // FIX: No esperar el reciclaje, usar fallback
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, 60000); // Máximo absoluto de 60 segundos
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) {