@mrxkun/mcfast-mcp 4.1.10 → 4.1.12

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.
@@ -2,6 +2,11 @@
2
2
  * File Watcher
3
3
  * Theo dõi thay đổi file và index tự động
4
4
  * Debounced 1.5s để batch rapid changes
5
+ *
6
+ * IMPROVEMENTS v4.1.10:
7
+ * - Added periodic cleanup for pendingUpdates Map
8
+ * - Added error boundaries for async operations
9
+ * - Added graceful shutdown cleanup
5
10
  */
6
11
 
7
12
  import chokidar from 'chokidar';
@@ -29,9 +34,13 @@ export class FileWatcher {
29
34
  this.watcher = null;
30
35
  this.pendingUpdates = new Map();
31
36
  this.isProcessing = false;
37
+ this.cleanupInterval = null;
38
+ this.isRunning = false;
32
39
 
33
40
  // Configuration
34
41
  this.debounceMs = options.debounceMs || 1500;
42
+ this.cleanupIntervalMs = options.cleanupIntervalMs || 30000; // Cleanup every 30s
43
+ this.maxPendingUpdates = options.maxPendingUpdates || 1000;
35
44
  this.ignored = options.ignored || [
36
45
  '**/node_modules/**',
37
46
  '**/.git/**',
@@ -46,40 +55,100 @@ export class FileWatcher {
46
55
  filesAdded: 0,
47
56
  filesChanged: 0,
48
57
  filesDeleted: 0,
49
- errors: 0
58
+ errors: 0,
59
+ totalProcessed: 0
50
60
  };
51
61
  }
52
62
 
53
63
  async start() {
64
+ if (this.isRunning) {
65
+ console.error('[FileWatcher] Already running');
66
+ return;
67
+ }
68
+
54
69
  console.error(`[FileWatcher] Starting watcher for: ${this.projectPath}`);
55
70
  console.error(`[FileWatcher] Debounce: ${this.debounceMs}ms`);
56
71
 
57
- this.watcher = chokidar.watch(this.projectPath, {
58
- ignored: this.ignored,
59
- persistent: true,
60
- ignoreInitial: true, // Don't trigger on existing files
61
- awaitWriteFinish: {
62
- stabilityThreshold: 300,
63
- pollInterval: 100
64
- }
65
- });
72
+ try {
73
+ this.watcher = chokidar.watch(this.projectPath, {
74
+ ignored: this.ignored,
75
+ persistent: true,
76
+ ignoreInitial: true,
77
+ awaitWriteFinish: {
78
+ stabilityThreshold: 300,
79
+ pollInterval: 100
80
+ }
81
+ });
66
82
 
67
- // Bind event handlers
68
- this.watcher.on('add', filePath => this.handleAdd(filePath));
69
- this.watcher.on('change', filePath => this.handleChange(filePath));
70
- this.watcher.on('unlink', filePath => this.handleDelete(filePath));
71
- this.watcher.on('error', error => this.handleError(error));
72
-
73
- // Setup debounced flush
74
- this.flushQueue = debounce(() => this.processQueue(), this.debounceMs);
83
+ // Bind event handlers with error handling
84
+ this.watcher.on('add', filePath => this.safeHandleAdd(filePath));
85
+ this.watcher.on('change', filePath => this.safeHandleChange(filePath));
86
+ this.watcher.on('unlink', filePath => this.safeHandleDelete(filePath));
87
+ this.watcher.on('error', error => this.handleError(error));
88
+
89
+ // Setup debounced flush
90
+ this.flushQueue = debounce(() => this.processQueue(), this.debounceMs);
91
+
92
+ // Wait for ready
93
+ await new Promise((resolve, reject) => {
94
+ this.watcher.once('ready', resolve);
95
+ this.watcher.once('error', reject);
96
+ });
97
+
98
+ // Start periodic cleanup
99
+ this.startCleanup();
100
+
101
+ this.isRunning = true;
102
+ console.error(`[FileWatcher] Ready and watching`);
103
+ } catch (error) {
104
+ console.error(`[FileWatcher] Failed to start:`, error.message);
105
+ throw error;
106
+ }
107
+ }
108
+
109
+ // Safe handlers with try-catch
110
+ safeHandleAdd(filePath) {
111
+ try {
112
+ this.handleAdd(filePath);
113
+ } catch (error) {
114
+ console.error(`[FileWatcher] Error in handleAdd:`, error.message);
115
+ this.stats.errors++;
116
+ }
117
+ }
118
+
119
+ safeHandleChange(filePath) {
120
+ try {
121
+ this.handleChange(filePath);
122
+ } catch (error) {
123
+ console.error(`[FileWatcher] Error in handleChange:`, error.message);
124
+ this.stats.errors++;
125
+ }
126
+ }
127
+
128
+ safeHandleDelete(filePath) {
129
+ try {
130
+ this.handleDelete(filePath);
131
+ } catch (error) {
132
+ console.error(`[FileWatcher] Error in handleDelete:`, error.message);
133
+ this.stats.errors++;
134
+ }
135
+ }
136
+
137
+ // Periodic cleanup to prevent memory leak
138
+ startCleanup() {
139
+ if (this.cleanupInterval) {
140
+ clearInterval(this.cleanupInterval);
141
+ }
75
142
 
76
- // Wait for ready
77
- await new Promise((resolve, reject) => {
78
- this.watcher.once('ready', resolve);
79
- this.watcher.once('error', reject);
80
- });
143
+ this.cleanupInterval = setInterval(() => {
144
+ if (this.pendingUpdates.size > this.maxPendingUpdates) {
145
+ console.error(`[FileWatcher] Cleanup: clearing ${this.pendingUpdates.size} pending updates`);
146
+ this.pendingUpdates.clear();
147
+ }
148
+ }, this.cleanupIntervalMs);
81
149
 
82
- console.error(`[FileWatcher] Ready and watching`);
150
+ // Prevent interval from keeping process alive
151
+ this.cleanupInterval.unref();
83
152
  }
84
153
 
85
154
  handleAdd(filePath) {
@@ -119,7 +188,6 @@ export class FileWatcher {
119
188
 
120
189
  async processQueue() {
121
190
  if (this.isProcessing) {
122
- // If already processing, debounce will call again
123
191
  return;
124
192
  }
125
193
 
@@ -139,14 +207,25 @@ export class FileWatcher {
139
207
 
140
208
  // Process deletions first
141
209
  for (const update of deletes) {
142
- await this.processDelete(update);
210
+ try {
211
+ await this.processDelete(update);
212
+ } catch (error) {
213
+ console.error(`[FileWatcher] Error processing delete:`, error.message);
214
+ this.stats.errors++;
215
+ }
143
216
  }
144
217
 
145
218
  // Process additions/changes
146
219
  for (const update of adds) {
147
- await this.processFile(update);
220
+ try {
221
+ await this.processFile(update);
222
+ } catch (error) {
223
+ console.error(`[FileWatcher] Error processing file:`, error.message);
224
+ this.stats.errors++;
225
+ }
148
226
  }
149
227
 
228
+ this.stats.totalProcessed += updates.length;
150
229
  console.error(`[FileWatcher] Processed ${updates.length} updates`);
151
230
 
152
231
  } catch (error) {
@@ -165,19 +244,19 @@ export class FileWatcher {
165
244
  const stats = await fs.stat(filePath).catch(() => null);
166
245
  if (!stats || !stats.isFile()) return;
167
246
 
168
- // Skip large files (> 1MB)
169
- if (stats.size > 1024 * 1024) {
170
- console.error(`[FileWatcher] Skipping large file: ${filePath} (${Math.round(stats.size / 1024)}KB)`);
171
- return;
172
- }
173
-
174
- // Read file content
247
+ // Read file content first
175
248
  const content = await fs.readFile(filePath, 'utf-8');
176
249
  const contentHash = crypto.createHash('md5').update(content).digest('hex');
177
250
 
251
+ // Handle large files (> 1MB)
252
+ if (stats.size > 1024 * 1024) {
253
+ console.error(`[FileWatcher] Large file detected: ${filePath} (${Math.round(stats.size / 1024)}KB)`);
254
+ return this.indexLargeFile(filePath, content, contentHash, stats);
255
+ }
256
+
178
257
  // Check if already indexed with same hash
179
- if (this.memory.codebaseDb?.isFileIndexed?.(filePath, contentHash)) {
180
- console.error(`[FileWatcher] File unchanged: ${filePath}`);
258
+ const existingFile = this.memory.codebaseDb?.getFileByPath?.(filePath);
259
+ if (existingFile && existingFile.content_hash === contentHash) {
181
260
  return;
182
261
  }
183
262
 
@@ -222,77 +301,102 @@ export class FileWatcher {
222
301
  async indexMarkdownFile(filePath, content, contentHash, stats) {
223
302
  console.error(`[FileWatcher] Indexing Markdown: ${filePath}`);
224
303
 
225
- // Delete old chunks if updating
226
- const relativePath = path.relative(this.projectPath, filePath);
227
- this.memory.memoryDb?.deleteChunksByFile?.(relativePath);
228
-
229
- // Chunk the content
230
- const { MarkdownChunker } = await import('../utils/markdown-chunker.js');
231
- const chunker = new MarkdownChunker();
232
- const chunks = chunker.chunk(content, relativePath);
233
-
234
- // Track file
235
- this.memory.memoryDb?.upsertFile?.(relativePath, contentHash, stats.mtimeMs, stats.size);
236
-
237
- // Generate embeddings and insert chunks
238
- for (const chunk of chunks) {
239
- // Check cache first
240
- let embedding = null;
241
- const cached = this.memory.memoryDb?.getCachedEmbedding?.(chunk.contentHash);
304
+ try {
305
+ // Delete old chunks if updating
306
+ const relativePath = path.relative(this.projectPath, filePath);
307
+ this.memory.memoryDb?.deleteChunksByFile?.(relativePath);
242
308
 
243
- if (cached) {
244
- embedding = cached.embedding;
245
- } else {
246
- // Generate embedding
247
- embedding = await this.memory.embedder?.embedCode?.(chunk.content);
248
-
249
- // Cache it
250
- if (embedding) {
251
- this.memory.memoryDb?.cacheEmbedding?.(
252
- chunk.contentHash,
253
- Buffer.from(new Float32Array(embedding).buffer),
254
- 'simple-embedder',
255
- embedding.length
256
- );
257
- }
258
- }
309
+ // Chunk the content
310
+ const { MarkdownChunker } = await import('../utils/markdown-chunker.js');
311
+ const chunker = new MarkdownChunker();
312
+ const chunks = chunker.chunk(content, relativePath);
259
313
 
260
- // Insert chunk
261
- this.memory.memoryDb?.insertChunk?.({
262
- id: chunk.id,
263
- file_path: chunk.filePath,
264
- start_line: chunk.startLine,
265
- end_line: chunk.endLine,
266
- content: chunk.content,
267
- content_hash: chunk.contentHash,
268
- chunk_type: chunk.chunkType
269
- });
314
+ // Track file
315
+ this.memory.memoryDb?.upsertFile?.(relativePath, contentHash, stats.mtimeMs, stats.size);
270
316
 
271
- // Insert embedding
272
- if (embedding) {
273
- this.memory.memoryDb?.insertEmbedding?.({
274
- chunk_id: chunk.id,
275
- embedding: Buffer.from(new Float32Array(embedding).buffer),
276
- model: 'simple-embedder',
277
- dimensions: embedding.length
278
- });
317
+ // Generate embeddings and insert chunks
318
+ for (const chunk of chunks) {
319
+ try {
320
+ // Check cache first
321
+ let embedding = null;
322
+ const cached = this.memory.memoryDb?.getCachedEmbedding?.(chunk.contentHash);
323
+
324
+ if (cached) {
325
+ embedding = cached.embedding;
326
+ } else {
327
+ // Generate embedding
328
+ embedding = await this.memory.embedder?.embedCode?.(chunk.content);
329
+
330
+ // Cache it
331
+ if (embedding) {
332
+ this.memory.memoryDb?.cacheEmbedding?.(
333
+ chunk.contentHash,
334
+ Buffer.from(new Float32Array(embedding).buffer),
335
+ 'simple-embedder',
336
+ embedding.length
337
+ );
338
+ }
339
+ }
340
+
341
+ // Insert chunk
342
+ this.memory.memoryDb?.insertChunk?.({
343
+ id: chunk.id,
344
+ file_path: chunk.filePath,
345
+ start_line: chunk.startLine,
346
+ end_line: chunk.endLine,
347
+ content: chunk.content,
348
+ content_hash: chunk.contentHash,
349
+ chunk_type: chunk.chunkType
350
+ });
351
+
352
+ // Insert embedding
353
+ if (embedding) {
354
+ this.memory.memoryDb?.insertEmbedding?.({
355
+ chunk_id: chunk.id,
356
+ embedding: Buffer.from(new Float32Array(embedding).buffer),
357
+ model: 'simple-embedder',
358
+ dimensions: embedding.length
359
+ });
360
+ }
361
+ } catch (chunkError) {
362
+ console.error(`[FileWatcher] Error indexing chunk:`, chunkError.message);
363
+ }
279
364
  }
365
+
366
+ console.error(`[FileWatcher] Indexed ${chunks.length} chunks from ${filePath}`);
367
+ } catch (error) {
368
+ console.error(`[FileWatcher] Error indexing markdown ${filePath}:`, error.message);
369
+ this.stats.errors++;
280
370
  }
281
-
282
- console.error(`[FileWatcher] Indexed ${chunks.length} chunks from ${filePath}`);
283
371
  }
284
372
 
285
- async indexCodeFile(filePath, content, contentHash, stats) {
373
+ async indexCodeFile(filePath, content, contentHash, stats, maxRetries = 3) {
286
374
  console.error(`[FileWatcher] Indexing code: ${filePath}`);
287
-
288
- // Use the existing indexer
289
- if (this.memory.indexer) {
375
+
376
+ if (!this.memory.indexer) {
377
+ console.error(`[FileWatcher] No indexer available`);
378
+ return;
379
+ }
380
+
381
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
290
382
  try {
291
383
  const indexed = await this.memory.indexer.indexFile(filePath, content);
292
- await this.memory.storeIndexed(indexed);
293
- console.error(`[FileWatcher] Indexed ${indexed.facts.length} facts, ${indexed.chunks.length} chunks from ${filePath}`);
384
+ const stored = await this.memory.storeIndexed(indexed);
385
+
386
+ if (stored) {
387
+ console.error(`[FileWatcher] Indexed ${indexed.facts.length} facts, ${indexed.chunks.length} chunks`);
388
+ return;
389
+ } else {
390
+ throw new Error('storeIndexed returned false');
391
+ }
294
392
  } catch (error) {
295
- console.error(`[FileWatcher] Failed to index ${filePath}:`, error.message);
393
+ if (attempt === maxRetries) {
394
+ console.error(`[FileWatcher] Failed to index after ${maxRetries} attempts:`, error.message);
395
+ this.stats.errors++;
396
+ } else {
397
+ console.warn(`[FileWatcher] Retry ${attempt}/${maxRetries}:`, error.message);
398
+ await new Promise(resolve => setTimeout(resolve, 100 * Math.pow(2, attempt - 1)));
399
+ }
296
400
  }
297
401
  }
298
402
  }
@@ -302,25 +406,75 @@ export class FileWatcher {
302
406
  return ext === '.md' || ext === '.markdown';
303
407
  }
304
408
 
409
+ async indexLargeFile(filePath, content, contentHash, stats) {
410
+ const chunkSize = 500 * 1024;
411
+ try {
412
+ const lines = content.split('\n');
413
+ let chunkContent = '';
414
+ let chunkIndex = 0;
415
+
416
+ for (let i = 0; i < lines.length; i++) {
417
+ chunkContent += lines[i] + '\n';
418
+
419
+ if (chunkContent.length >= chunkSize || i === lines.length - 1) {
420
+ console.error(`[FileWatcher] Indexing chunk ${chunkIndex + 1} of large file`);
421
+
422
+ if (this.isMarkdownFile(filePath)) {
423
+ await this.indexMarkdownFile(filePath, chunkContent, contentHash + '_' + chunkIndex, stats);
424
+ } else {
425
+ await this.indexCodeFile(filePath, chunkContent, contentHash + '_' + chunkIndex, stats);
426
+ }
427
+
428
+ chunkIndex++;
429
+ chunkContent = '';
430
+ }
431
+ }
432
+
433
+ console.error(`[FileWatcher] Large file indexed in ${chunkIndex} chunks`);
434
+ this.stats.filesChanged++;
435
+
436
+ } catch (error) {
437
+ console.error(`[FileWatcher] Failed to index large file:`, error.message);
438
+ this.stats.errors++;
439
+ }
440
+ }
441
+
305
442
  getStats() {
306
443
  return {
307
444
  ...this.stats,
308
445
  pendingUpdates: this.pendingUpdates.size,
309
- isProcessing: this.isProcessing
446
+ isProcessing: this.isProcessing,
447
+ isRunning: this.isRunning
310
448
  };
311
449
  }
312
450
 
313
451
  async stop() {
452
+ if (!this.isRunning) return;
453
+
454
+ console.error('[FileWatcher] Stopping...');
455
+
456
+ // 1. Stop cleanup interval
457
+ if (this.cleanupInterval) {
458
+ clearInterval(this.cleanupInterval);
459
+ this.cleanupInterval = null;
460
+ }
461
+
462
+ // 2. Clear pending updates map (free memory)
463
+ this.pendingUpdates.clear();
464
+
465
+ // 3. Cancel debounce timeout if exists
466
+ if (this.flushQueue && this.flushQueue.cancel) {
467
+ this.flushQueue.cancel();
468
+ }
469
+
470
+ // 4. Close chokidar watcher (critical - prevents persistent file watching)
314
471
  if (this.watcher) {
315
- // Process any remaining updates
316
- if (this.pendingUpdates.size > 0) {
317
- await this.processQueue();
318
- }
319
-
320
472
  await this.watcher.close();
321
473
  this.watcher = null;
322
- console.error(`[FileWatcher] Stopped`);
323
474
  }
475
+
476
+ this.isRunning = false;
477
+ console.error('[FileWatcher] Stopped');
324
478
  }
325
479
  }
326
480