A memory-based implementation of Node.js fs module for testing purposes
Overall
score
96%
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.
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 renamedUsage 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);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']);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;
}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);
});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');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();
}
}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);
}
});
}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-fsdocs
evals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10