or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

index.md
tile.json

index.mddocs/

fb-watchman

fb-watchman provides Node.js bindings for Facebook's Watchman file watching service, enabling efficient filesystem monitoring and change detection in JavaScript applications. It offers a Client class that communicates with the Watchman daemon through a socket connection using the BSER (Binary Serialization) protocol, supporting advanced file querying operations and change subscriptions.

Package Information

  • Package Name: fb-watchman
  • Package Type: npm
  • Language: JavaScript
  • Installation: npm install fb-watchman
  • Dependencies: bser@2.1.1 (Binary Serialization - automatically installed)

Core Imports

const watchman = require('fb-watchman');
const client = new watchman.Client();

For ES modules:

import watchman from 'fb-watchman';
const client = new watchman.Client();

Basic Usage

const watchman = require('fb-watchman');
const client = new watchman.Client();

// Handle connection events
client.on('connect', () => {
  console.log('Connected to Watchman');
});

client.on('error', (error) => {
  console.error('Error:', error);
});

// Check capabilities
client.capabilityCheck({ required: ['relative_root'] }, (error, resp) => {
  if (error) {
    console.error('Capability check failed:', error);
    return;
  }
  console.log('Watchman version:', resp.version);
});

// Watch a project directory
client.command(['watch-project', process.cwd()], (error, resp) => {
  if (error) {
    console.error('Watch failed:', error);
    return;
  }
  
  console.log('Watching:', resp.watch);
  
  // Subscribe to file changes
  client.command(['subscribe', resp.watch, 'mysubscription', {
    expression: ['allof', ['match', '*.js']],
    fields: ['name', 'size', 'exists', 'type']
  }], (error, resp) => {
    if (error) {
      console.error('Subscribe failed:', error);
      return;
    }
    console.log('Subscription established:', resp.subscribe);
  });
});

// Listen for file change notifications
client.on('subscription', (resp) => {
  console.log('Files changed:', resp.files);
});

// Clean up
process.on('exit', () => {
  client.end();
});

Architecture

fb-watchman is built around several key components:

  • Client Class: Main interface that extends EventEmitter for handling connections and communication
  • BSER Protocol: Binary serialization format for efficient communication with Watchman daemon (handled by bser dependency)
  • Connection Management: Automatic socket connection handling with fallback to process spawning
  • Command Queue: Queued command system ensuring proper request/response ordering
  • Capability System: Version-aware feature detection for compatibility with different Watchman versions
  • Event System: EventEmitter-based architecture for handling subscriptions and notifications

Capabilities

Client Constructor

Creates a new Watchman client instance with optional configuration.

/**
 * Creates a new Watchman client instance
 * @param {Object} options - Optional configuration
 * @param {string} options.watchmanBinaryPath - Absolute path to watchman binary
 */
function Client(options);

Connection Management

Establishes connection to the Watchman service, either via existing socket or by spawning the Watchman process.

/**
 * Establishes connection to Watchman service
 */
Client.prototype.connect();

/**
 * Closes the connection to the Watchman service
 */
Client.prototype.end();

Command Execution

Sends commands to the Watchman service with automatic connection and queuing.

/**
 * Sends a command to the Watchman service
 * @param {Array} args - Command arguments array
 * @param {Function} done - Optional callback function for response
 */
Client.prototype.command(args, done);

Usage Examples:

// Watch a directory
client.command(['watch-project', '/path/to/project'], (error, resp) => {
  if (error) {
    console.error('Watch failed:', error);
    return;
  }
  console.log('Watching:', resp.watch);
});

// Query files
client.command(['query', '/path/to/project', {
  expression: ['allof', ['type', 'f'], ['suffix', 'js']],
  fields: ['name', 'size', 'mtime_ms']
}], (error, resp) => {
  if (error) {
    console.error('Query failed:', error);
    return;
  }
  console.log('Files:', resp.files);
});

// Create subscription
client.command(['subscribe', '/path/to/project', 'mysubscription', {
  expression: ['allof', ['match', '*.js']],
  fields: ['name', 'size', 'exists', 'type']
}], (error, resp) => {
  if (error) {
    console.error('Subscribe failed:', error);
    return;
  }
  console.log('Subscription:', resp.subscribe);
});

// Unsubscribe
client.command(['unsubscribe', '/path/to/project', 'mysubscription'], (error, resp) => {
  if (error) {
    console.error('Unsubscribe failed:', error);
    return;
  }
  console.log('Unsubscribed from:', resp.unsubscribe);
});

// Get server version
client.command(['version'], (error, resp) => {
  if (error) {
    console.error('Version query failed:', error);
    return;
  }
  console.log('Watchman version:', resp.version);
});

Capability Checking

Checks if the Watchman server supports specific capabilities, ensuring compatibility across versions.

/**
 * Checks if Watchman server supports specified capabilities
 * @param {Object} caps - Capability requirements
 * @param {Array} caps.optional - List of optional capabilities
 * @param {Array} caps.required - List of required capabilities
 * @param {Function} done - Callback function for capability check results
 */
Client.prototype.capabilityCheck(caps, done);

/**
 * Internal helper for testing - synthesizes capability check results for older Watchman versions
 * @param {Object} resp - Version response object
 * @param {Array} optional - List of optional capabilities
 * @param {Array} required - List of required capabilities
 * @returns {Object} Response with synthesized capabilities
 */
Client.prototype._synthesizeCapabilityCheck(resp, optional, required);

Usage Examples:

// Check required capabilities
client.capabilityCheck({
  required: ['relative_root', 'wildmatch']
}, (error, resp) => {
  if (error) {
    console.error('Missing required capabilities:', error);
    return;
  }
  console.log('All required capabilities supported');
  console.log('Capabilities:', resp.capabilities);
});

// Check optional capabilities
client.capabilityCheck({
  optional: ['term-dirname', 'cmd-watch-del-all'],
  required: ['relative_root']
}, (error, resp) => {
  if (error) {
    console.error('Capability check failed:', error);
    return;
  }
  
  if (resp.capabilities['term-dirname']) {
    console.log('Advanced directory matching supported');
  }
  
  if (resp.capabilities['cmd-watch-del-all']) {
    console.log('Watch deletion command supported');
  }
});

Known Capability Requirements:

The client internally tracks minimum Watchman versions for various capabilities:

  • cmd-watch-del-all: Requires Watchman 3.1.1+
  • cmd-watch-project: Requires Watchman 3.1+
  • relative_root: Requires Watchman 3.3+
  • term-dirname: Requires Watchman 3.1+
  • term-idirname: Requires Watchman 3.1+
  • wildmatch: Requires Watchman 3.7+

Event Handling

The Client class extends EventEmitter and emits several events for handling connections and data.

/**
 * Events emitted by Client instance:
 * 
 * 'connect' - Emitted when connection to Watchman is established
 * 'error' - Emitted when an error occurs (error)
 * 'end' - Emitted when connection to Watchman is terminated
 * 'subscription' - Emitted when receiving file change notifications (response)
 * 'log' - Emitted when receiving log messages from Watchman (response)
 */

Usage Examples:

// Connection lifecycle
client.on('connect', () => {
  console.log('Connected to Watchman daemon');
});

client.on('end', () => {
  console.log('Connection to Watchman closed');
});

client.on('error', (error) => {
  console.error('Watchman error:', error);
  if (error.watchmanResponse) {
    console.error('Watchman response:', error.watchmanResponse);
  }
});

// File change notifications
client.on('subscription', (resp) => {
  console.log('Subscription:', resp.subscription);
  console.log('Root directory:', resp.root);
  
  resp.files.forEach(file => {
    console.log('File changed:', file.name);
    console.log('Size:', file.size);
    console.log('Exists:', file.exists);
    console.log('Type:', file.type);
  });
});

// Log messages from Watchman
client.on('log', (resp) => {
  console.log('Watchman log:', resp.log);
});

Types

/**
 * Client constructor options
 */
interface ClientOptions {
  /** Absolute path to the watchman binary executable */
  watchmanBinaryPath?: string;
}

/**
 * Capability check parameters
 */
interface CapabilityCheck {
  /** List of optional capabilities to check */
  optional?: string[];
  /** List of required capabilities that must be supported */
  required?: string[];
}

/**
 * Capability check response
 */
interface CapabilityResponse {
  /** Watchman server version */
  version: string;
  /** Map of capability names to support status */
  capabilities: { [capability: string]: boolean };
  /** Error message if required capabilities are missing */
  error?: string;
}

/**
 * Watch project response
 */
interface WatchResponse {
  /** Root directory being watched */
  watch: string;
  /** Relative path if using existing watch at higher level */
  relative_path?: string;
  /** Warning message from Watchman */
  warning?: string;
}

/**
 * Subscription response
 */
interface SubscriptionResponse {
  /** Subscription name */
  subscribe: string;
  /** Root directory being watched */
  root: string;
  /** Array of file objects matching subscription criteria */
  files: FileInfo[];
  /** Subscription name for notifications */
  subscription: string;
}

/**
 * File information object
 */
interface FileInfo {
  /** File name relative to root */
  name: string;
  /** File size in bytes */
  size?: number;
  /** Whether file exists */
  exists?: boolean;
  /** File type ('f' for file, 'd' for directory) */
  type?: string;
  /** File mode/permissions */
  mode?: number;
  /** Modification time in milliseconds */
  mtime_ms?: number;
  /** Whether this is a new file */
  new?: boolean;
}

/**
 * Query expression for file matching
 */
type QueryExpression = 
  | ['allof', ...QueryExpression[]]
  | ['anyof', ...QueryExpression[]]
  | ['not', QueryExpression]
  | ['match', string]
  | ['imatch', string]
  | ['pcre', string]
  | ['ipcre', string]
  | ['name', string]
  | ['iname', string]
  | ['type', 'f' | 'd' | 'l']
  | ['suffix', string]
  | ['dirname', string]
  | ['idirname', string];

/**
 * Common Watchman commands
 */
type WatchmanCommand = 
  | ['version']
  | ['version', { optional?: string[]; required?: string[] }]
  | ['watch-project', string]
  | ['query', string, { expression?: QueryExpression; fields?: string[] }]
  | ['subscribe', string, string, { expression?: QueryExpression; fields?: string[]; relative_root?: string; since?: string }]
  | ['unsubscribe', string, string]
  | ['clock', string];

Error Handling

fb-watchman provides comprehensive error handling for various failure scenarios:

Connection Errors:

  • ENOENT - Watchman binary not found in PATH (with helpful installation message)
  • EACCES - Permission denied when executing Watchman binary
  • Socket connection failures when connecting to existing daemon

Communication Errors:

  • BSER protocol parsing errors during data transmission
  • Command timeout errors for long-running operations
  • Connection termination during command execution

Watchman Service Errors:

  • Invalid command syntax (returns error object with watchmanResponse property)
  • File system permission issues during watch setup
  • Watch limit exceeded (system-dependent)
  • Subscription errors due to malformed expressions
  • Missing required capabilities (synthesized by client for older versions)

Error Objects:

// Errors include watchmanResponse property when available
try {
  client.command(['invalid-command'], (error, resp) => {
    if (error) {
      console.error('Command failed:', error.message);
      if (error.watchmanResponse) {
        console.error('Watchman details:', error.watchmanResponse.error);
      }
    }
  });
} catch (error) {
  console.error('Unexpected error:', error);
}