CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-metro-memory-fs

A memory-based implementation of Node.js fs module for testing purposes

Overall
score

96%

Overview
Eval results
Files

file-watching.mddocs/

File Watching

File system monitoring capabilities with event-driven notifications for file and directory changes, supporting recursive watching and various encoding options. Provides real-time monitoring of filesystem changes.

Capabilities

File System Watching

Monitor files and directories for changes with event-based notifications.

/**
 * Watch for changes on a file or directory
 * @param filename - File or directory path to watch
 * @param options - Watch options including encoding and recursion
 * @param listener - Event listener function (optional)
 * @returns FSWatcher instance
 */
watch(filename: string | Buffer, options?: WatchOptions, listener?: WatchListener): FSWatcher;
watch(filename: string | Buffer, listener: WatchListener): FSWatcher;
watch(filename: string | Buffer, encoding: string, listener: WatchListener): FSWatcher;

interface WatchOptions {
  /** Encoding for filename in events */
  encoding?: string;
  /** Keep process alive while watching */
  persistent?: boolean;
  /** Watch subdirectories recursively */
  recursive?: boolean;
}

type WatchListener = (eventType: 'rename' | 'change', filename?: string | Buffer) => void;

interface FSWatcher extends EventEmitter {
  /** Close the watcher and stop monitoring */
  close(): void;
  
  // Event emitters
  on(event: 'change', listener: WatchListener): this;
  on(event: 'error', listener: (error: Error) => void): this;
  on(event: 'close', listener: () => void): this;
}

Event Types:

  • 'change' - File content was modified
  • 'rename' - File was created, deleted, or renamed

Usage Examples:

// Basic file watching
const watcher = fs.watch('/important.txt', (eventType, filename) => {
  console.log(`Event: ${eventType} on ${filename}`);
});

// Watch with options
const dirWatcher = fs.watch('/project', {
  recursive: true,
  encoding: 'utf8'
}, (eventType, filename) => {
  console.log(`${eventType}: ${filename}`);
});

// Watch with event listeners
const fileWatcher = fs.watch('/config.json');
fileWatcher.on('change', (eventType, filename) => {
  if (eventType === 'change') {
    console.log('File content changed:', filename);
    // Reload configuration
    reloadConfig();
  } else if (eventType === 'rename') {
    console.log('File was renamed or deleted:', filename);
  }
});

fileWatcher.on('error', (err) => {
  console.error('Watcher error:', err);
});

fileWatcher.on('close', () => {
  console.log('Watcher closed');
});

// Close watcher when done
setTimeout(() => {
  fileWatcher.close();
}, 10000);

Directory Watching

Monitor directories for file and subdirectory changes.

// Watch directory for file changes
const dirWatcher = fs.watch('/uploads', (eventType, filename) => {
  if (eventType === 'rename') {
    if (fs.existsSync(`/uploads/${filename}`)) {
      console.log('New file added:', filename);
    } else {
      console.log('File removed:', filename);
    }
  } else if (eventType === 'change') {
    console.log('File modified:', filename);
  }
});

// Recursive directory watching
const projectWatcher = fs.watch('/project', { recursive: true }, (eventType, filename) => {
  console.log(`Project change - ${eventType}: ${filename}`);
  
  // Handle different file types
  if (filename.endsWith('.js')) {
    console.log('JavaScript file changed, may need restart');
  } else if (filename.endsWith('.json')) {
    console.log('Config file changed, reloading...');
  }
});

// Watch multiple directories
function watchMultiple(paths) {
  const watchers = [];
  
  paths.forEach(path => {
    const watcher = fs.watch(path, { recursive: true }, (eventType, filename) => {
      console.log(`[${path}] ${eventType}: ${filename}`);
    });
    watchers.push(watcher);
  });
  
  // Return cleanup function
  return () => {
    watchers.forEach(watcher => watcher.close());
  };
}

const cleanup = watchMultiple(['/src', '/config', '/public']);

Event Handling Patterns

Advanced patterns for handling file system events.

// Debounced file watching (avoid multiple rapid events)
function createDebouncedWatcher(path, delay = 100) {
  const timers = new Map();
  
  return fs.watch(path, { recursive: true }, (eventType, filename) => {
    const key = `${eventType}:${filename}`;
    
    // Clear existing timer
    if (timers.has(key)) {
      clearTimeout(timers.get(key));
    }
    
    // Set new timer
    const timer = setTimeout(() => {
      console.log(`Debounced event - ${eventType}: ${filename}`);
      timers.delete(key);
      // Handle the event here
      handleFileChange(eventType, filename);
    }, delay);
    
    timers.set(key, timer);
  });
}

// Filter events by file type
function createFilteredWatcher(path, extensions) {
  return fs.watch(path, { recursive: true }, (eventType, filename) => {
    if (!filename) return;
    
    const ext = filename.split('.').pop()?.toLowerCase();
    if (extensions.includes(ext)) {
      console.log(`${eventType} on ${ext} file: ${filename}`);
      processFileChange(eventType, filename);
    }
  });
}

// Usage: only watch JavaScript and TypeScript files
const codeWatcher = createFilteredWatcher('/src', ['js', 'ts', 'jsx', 'tsx']);

// Event aggregation
function createAggregatingWatcher(path, flushInterval = 1000) {
  const events = [];
  
  const watcher = fs.watch(path, { recursive: true }, (eventType, filename) => {
    events.push({ eventType, filename, timestamp: Date.now() });
  });
  
  // Flush events periodically
  const interval = setInterval(() => {
    if (events.length > 0) {
      console.log(`Processing ${events.length} events:`);
      events.forEach(event => {
        console.log(`  ${event.eventType}: ${event.filename}`);
      });
      
      processBatchedEvents([...events]);
      events.length = 0; // Clear events
    }
  }, flushInterval);
  
  // Return watcher with custom close method
  const originalClose = watcher.close.bind(watcher);
  watcher.close = () => {
    clearInterval(interval);
    originalClose();
  };
  
  return watcher;
}

Configuration File Watching

Monitor configuration files and reload application state.

// Configuration file watcher with reload
class ConfigWatcher {
  constructor(configPath, onReload) {
    this.configPath = configPath;
    this.onReload = onReload;
    this.config = this.loadConfig();
    this.watcher = null;
    this.startWatching();
  }
  
  loadConfig() {
    try {
      const content = fs.readFileSync(this.configPath, 'utf8');
      return JSON.parse(content);
    } catch (err) {
      console.error('Error loading config:', err);
      return {};
    }
  }
  
  startWatching() {
    this.watcher = fs.watch(this.configPath, (eventType) => {
      if (eventType === 'change') {
        console.log('Config file changed, reloading...');
        const newConfig = this.loadConfig();
        
        if (JSON.stringify(newConfig) !== JSON.stringify(this.config)) {
          this.config = newConfig;
          this.onReload(newConfig);
        }
      }
    });
    
    this.watcher.on('error', (err) => {
      console.error('Config watcher error:', err);
    });
  }
  
  stop() {
    if (this.watcher) {
      this.watcher.close();
      this.watcher = null;
    }
  }
  
  getConfig() {
    return this.config;
  }
}

// Usage
const configWatcher = new ConfigWatcher('/app/config.json', (newConfig) => {
  console.log('Configuration updated:', newConfig);
  // Update application settings
  updateAppSettings(newConfig);
});

Build System File Watching

File watching patterns commonly used in build systems and development tools.

// Development server with auto-reload
class DevServer {
  constructor(srcPath, buildPath) {
    this.srcPath = srcPath;
    this.buildPath = buildPath;
    this.building = false;
    this.pendingBuild = false;
    
    this.watcher = fs.watch(srcPath, { recursive: true }, (eventType, filename) => {
      if (filename && this.shouldTriggerBuild(filename)) {
        this.scheduleBuild();
      }
    });
  }
  
  shouldTriggerBuild(filename) {
    // Only build for source files
    const sourceExtensions = ['js', 'ts', 'jsx', 'tsx', 'css', 'scss'];
    const ext = filename.split('.').pop()?.toLowerCase();
    return sourceExtensions.includes(ext);
  }
  
  scheduleBuild() {
    if (this.building) {
      this.pendingBuild = true;
      return;
    }
    
    // Debounce builds
    setTimeout(() => {
      this.runBuild();
    }, 200);
  }
  
  async runBuild() {
    if (this.building) return;
    
    this.building = true;
    console.log('Building...');
    
    try {
      await this.performBuild();
      console.log('Build completed');
      
      if (this.pendingBuild) {
        this.pendingBuild = false;
        setTimeout(() => this.runBuild(), 100);
      }
    } catch (err) {
      console.error('Build failed:', err);
    } finally {
      this.building = false;
    }
  }
  
  async performBuild() {
    // Simulate build process
    const sourceFiles = this.getAllSourceFiles(this.srcPath);
    
    for (const file of sourceFiles) {
      const content = fs.readFileSync(file, 'utf8');
      const processed = this.processFile(content);
      
      const outputPath = file.replace(this.srcPath, this.buildPath);
      fs.mkdirSync(path.dirname(outputPath), { recursive: true });
      fs.writeFileSync(outputPath, processed);
    }
  }
  
  getAllSourceFiles(dir) {
    const files = [];
    const entries = fs.readdirSync(dir, { withFileTypes: true });
    
    for (const entry of entries) {
      const fullPath = `${dir}/${entry.name}`;
      if (entry.isDirectory()) {
        files.push(...this.getAllSourceFiles(fullPath));
      } else if (this.shouldTriggerBuild(entry.name)) {
        files.push(fullPath);
      }
    }
    
    return files;
  }
  
  processFile(content) {
    // Simulate file processing
    return content.replace(/\/\*\s*DEBUG\s*\*\/.*?\/\*\s*END_DEBUG\s*\*\//gs, '');
  }
  
  stop() {
    if (this.watcher) {
      this.watcher.close();
    }
  }
}

// Usage
const devServer = new DevServer('/src', '/build');

Testing File Watching

File watching utilities for testing scenarios.

// Test helper for file watching
class FileWatchTester {
  constructor() {
    this.events = [];
    this.watchers = [];
  }
  
  watchFile(path) {
    const watcher = fs.watch(path, (eventType, filename) => {
      this.events.push({
        eventType,
        filename,
        timestamp: Date.now(),
        path
      });
    });
    
    this.watchers.push(watcher);
    return watcher;
  }
  
  clearEvents() {
    this.events = [];
  }
  
  getEvents() {
    return [...this.events];
  }
  
  waitForEvent(timeout = 1000) {
    return new Promise((resolve, reject) => {
      const startLength = this.events.length;
      const timer = setTimeout(() => {
        reject(new Error('Timeout waiting for file event'));
      }, timeout);
      
      const checkForEvent = () => {
        if (this.events.length > startLength) {
          clearTimeout(timer);
          resolve(this.events[this.events.length - 1]);
        } else {
          setTimeout(checkForEvent, 10);
        }
      };
      
      checkForEvent();
    });
  }
  
  cleanup() {
    this.watchers.forEach(watcher => watcher.close());
    this.watchers = [];
    this.events = [];
  }
}

// Usage in tests
async function testFileWatching() {
  const tester = new FileWatchTester();
  
  try {
    // Create test file
    fs.writeFileSync('/test.txt', 'initial content');
    
    // Start watching
    tester.watchFile('/test.txt');
    
    // Modify file
    fs.writeFileSync('/test.txt', 'modified content');
    
    // Wait for event
    const event = await tester.waitForEvent();
    console.log('Received event:', event);
    
    // Check event type
    assert.equal(event.eventType, 'change');
    assert.equal(event.filename, '/test.txt');
  } finally {
    tester.cleanup();
  }
}

Encoding Options

Handle different filename encodings in watch events.

// Watch with buffer encoding
const bufferWatcher = fs.watch('/files', { encoding: 'buffer' }, (eventType, filename) => {
  if (filename instanceof Buffer) {
    console.log('Filename as buffer:', filename);
    console.log('Filename as string:', filename.toString('utf8'));
  }
});

// Watch with hex encoding
const hexWatcher = fs.watch('/files', { encoding: 'hex' }, (eventType, filename) => {
  console.log('Filename in hex:', filename);
  const originalName = Buffer.from(filename, 'hex').toString('utf8');
  console.log('Original filename:', originalName);
});

// Handle encoding errors gracefully
function createSafeWatcher(path, encoding = 'utf8') {
  return fs.watch(path, { encoding }, (eventType, filename) => {
    try {
      if (filename) {
        console.log(`${eventType}: ${filename}`);
      } else {
        console.log(`${eventType}: (filename not provided)`);
      }
    } catch (err) {
      console.error('Error processing watch event:', err);
    }
  });
}

Error Handling and Cleanup

Proper error handling and resource cleanup for file watchers.

// Robust watcher with error handling
function createRobustWatcher(path, options = {}) {
  let watcher = null;
  let reconnectTimer = null;
  const maxRetries = 5;
  let retryCount = 0;
  
  function startWatcher() {
    try {
      watcher = fs.watch(path, options, (eventType, filename) => {
        retryCount = 0; // Reset retry count on successful event
        console.log(`${eventType}: ${filename}`);
      });
      
      watcher.on('error', (err) => {
        console.error('Watcher error:', err);
        
        if (retryCount < maxRetries) {
          retryCount++;
          console.log(`Attempting to reconnect (${retryCount}/${maxRetries})...`);
          
          reconnectTimer = setTimeout(() => {
            startWatcher();
          }, 1000 * retryCount); // Exponential backoff
        } else {
          console.error('Max retries reached, giving up');
        }
      });
      
      watcher.on('close', () => {
        console.log('Watcher closed');
      });
      
      console.log('Watcher started successfully');
    } catch (err) {
      console.error('Failed to start watcher:', err);
    }
  }
  
  startWatcher();
  
  // Return cleanup function
  return () => {
    if (reconnectTimer) {
      clearTimeout(reconnectTimer);
    }
    if (watcher) {
      watcher.close();
    }
  };
}

// Usage
const cleanup = createRobustWatcher('/important-files', { recursive: true });

// Clean up when done
process.on('SIGINT', () => {
  cleanup();
  process.exit(0);
});

Install with Tessl CLI

npx tessl i tessl/npm-metro-memory-fs

docs

directory-operations.md

file-descriptors.md

file-operations.md

file-watching.md

index.md

stats-permissions.md

streams.md

symbolic-links.md

tile.json