NodeJS
To install the nodejs client:
$ npm install fb-watchman
and to import it and create a client instance:
var watchman = require('fb-watchman');
var client = new watchman.Client();
This documentation assumes that you are using the latest available version of
the fb-watchman
package published to the npm repository.
Checking for watchman availability
The client can be installed without requiring that the service is installed. It is important to handle lack of availability and also to test whether the installed service supports the capabilities required by your application.
The capabilityCheck
method issues a version command to query
the capabilities of the server.
var watchman = require('fb-watchman');
var client = new watchman.Client();
client.capabilityCheck({optional:[], required:['relative_root']},
function (error, resp) {
if (error) {
// error will be an Error object if the watchman service is not
// installed, or if any of the names listed in the `required`
// array are not supported by the server
console.error(error);
}
// resp will be an extended version response:
// {'version': '3.8.0', 'capabilities': {'relative_root': true}}
console.log(resp);
});
Initiating a watch
Almost every operation in watchman revolves around watching a directory tree. You can repeatedly ask to watch the same directory without error; watchman will re-use an existing watch.
var watchman = require('fb-watchman');
var client = new watchman.Client();
var dir_of_interest = "/some/path";
client.capabilityCheck({optional:[], required:['relative_root']},
function (error, resp) {
if (error) {
console.log(error);
client.end();
return;
}
// Initiate the watch
client.command(['watch-project', dir_of_interest],
function (error, resp) {
if (error) {
console.error('Error initiating watch:', error);
return;
}
// It is considered to be best practice to show any 'warning' or
// 'error' information to the user, as it may suggest steps
// for remediation
if ('warning' in resp) {
console.log('warning: ', resp.warning);
}
// `watch-project` can consolidate the watch for your
// dir_of_interest with another watch at a higher level in the
// tree, so it is very important to record the `relative_path`
// returned in resp
console.log('watch established on ', resp.watch,
' relative_path', resp.relative_path);
});
});
Subscribing to changes
Most node applications are interested in subscribing to live file change notifications. In watchman these are configured by issuing a subscribe command. A subscription is valid for the duration of your client connection, or until you cancel the subscription using the unsubscribe command.
The following will generate subscription results for all files in the tree that match the query expression and then generate subscription results as files change:
// `watch` is obtained from `resp.watch` in the `watch-project` response.
// `relative_path` is obtained from `resp.relative_path` in the
// `watch-project` response.
function make_subscription(client, watch, relative_path) {
sub = {
// Match any `.js` file in the dir_of_interest
expression: ["allof", ["match", "*.js"]],
// Which fields we're interested in
fields: ["name", "size", "mtime_ms", "exists", "type"]
};
if (relative_path) {
sub.relative_root = relative_path;
}
client.command(['subscribe', watch, 'mysubscription', sub],
function (error, resp) {
if (error) {
// Probably an error in the subscription criteria
console.error('failed to subscribe: ', error);
return;
}
console.log('subscription ' + resp.subscribe + ' established');
});
// Subscription results are emitted via the subscription event.
// Note that this emits for all subscriptions. If you have
// subscriptions with different `fields` you will need to check
// the subscription name and handle the differing data accordingly.
// `resp` looks like this in practice:
//
// { root: '/private/tmp/foo',
// subscription: 'mysubscription',
// files: [ { name: 'node_modules/fb-watchman/index.js',
// size: 4768,
// exists: true,
// type: 'f' } ] }
client.on('subscription', function (resp) {
if (resp.subscription !== 'mysubscription') return;
resp.files.forEach(function (file) {
// convert Int64 instance to javascript integer
const mtime_ms = +file.mtime_ms;
console.log('file changed: ' + file.name, mtime_ms);
});
});
}
Subscribing only to changed files
The example above will generate results for existing (and deleted!) files at the time that the subscription is established. In some applications this can be undesirable. The following example shows how to add a logical time constraint.
watchman tracks changes using an abstract clock. We'll determine the current clock at the time that we initiate the watch and then add that as a constraint in our subscription.
function make_time_constrained_subscription(client, watch, relative_path) {
client.command(['clock', watch], function (error, resp) {
if (error) {
console.error('Failed to query clock:', error);
return;
}
sub = {
// Match any `.js` file in the dir_of_interest
expression: ["allof", ["match", "*.js"]],
// Which fields we're interested in
fields: ["name", "size", "exists", "type"],
// add our time constraint
since: resp.clock
};
if (relative_path) {
sub.relative_root = relative_path;
}
client.command(['subscribe', watch, 'mysubscription', sub],
function (error, resp) {
// handle the result here
});
});
}
NodeJS API Reference
Methods
client.capabilityCheck(options, done)
The capabilityCheck
method issues a version command to query
the capabilities of the server.
If the server doesn't support capabilities, capabilityCheck
will emulate the
capability response for a handful of significant capabilities based on the
version reported by the server.
The options
argument may contain the following properties:
optional
an array listing optional capability namesrequired
an array listing required capability names
The properties are passed through to the underlying version
command.
The done
parameter is a callback that will be passed (error, result) when the
command completes. It doesn't make sense to issue a capabilityCheck
call and
not provide the done
callback.
The response object will contain a capabilities
object property whose keys
will be the union of the optional
and required
capability names and whose
values will be either true
or false
depending on the availability of the
capability name.
If any of the required
capabilities are not supported by the server, the
error
parameter in the done
callback will be set and will contain a
meaningful error message.
client.capabilityCheck({optional:[], required:['relative_root']},
function (error, resp) {
if (error) {
// error will be an Error object if the watchman service is not
// installed, or if any of the names listed in the `required`
// array are not supported by the server
console.error(error);
}
// resp will be an extended version response:
// {'version': '3.8.0', 'capabilities': {'relative_root': true}}
console.log(resp);
});
client.command(args [, done])
Sends a command to the watchman service. args
is an array that specifies the
command name and any optional arguments. The command is queued and dispatched
asynchronously. You may queue multiple commands to the service; they will be
dispatched in FIFO order once the client connection is established.
The done
parameter is a callback that will be passed (error, result) when the
command completes. You may omit it if you are not interested in the result of
the command.
client.command(['watch-project', process.cwd()], function(error, resp) {
if (error) {
console.log('watch failed: ', error);
return;
}
if ('warning' in resp) {
console.log('warning: ', resp.warning);
}
if ('relative_path' in resp) {
// We will need to remember and adjust for relative_path
console.log('watching project ', resp.watch, ' relative path to cwd is ',
resp.relative_path);
} else {
console.log('watching ', resp.watch);
}
});
If a field named warning
is present in resp
, the watchman service is trying
to communicate an issue that the user should see and address. For example, if
the system watch resources need adjustment, watchman will provide information
about this and how to remediate the issue. It is suggested that tools that build
on top of this library bubble the warning message up to the user.
client.end()
Terminates the connection to the watchman service. Does not wait for any queued commands to send.
Events
The following events are emitted by the watchman client object:
Event: 'connect'
Emitted when the client successfully connects to the watchman service
Event: 'error'
Emitted when the socket to the watchman service encounters an error.
It may also be emitted prior to establishing a connection if we are unable to successfully execute the watchman CLI binary to determine how to talk to the server process.
It is passed a variable that encapsulates the error.
Event: 'end'
Emitted when the socket to the watchman service is closed
Event: 'log'
Emitted in response to a unilateral log
PDU from the watchman service. To
enable these, you need to send a log-level
command to the service:
// This is very verbose, you probably don't want to do this
client.command(['log-level', 'debug']);
client.on('log', function(info) {
console.log(info);
});
Event: 'subscription'
Emitted in response to a unilateral subscription
PDU from the watchman
service. To enable these, you need to send a subscribe
command to the service:
// Subscribe to notifications about .js files
client.command(['subscribe', process.cwd(), 'mysubscription', {
expression: ["match", "*.js"]
}],
function(error, resp) {
if (error) {
// Probably an error in the subscription criteria
console.log('failed to subscribe: ', error);
return;
}
console.log('subscription ' + resp.subscribe + ' established');
}
);
// Subscription results are emitted via the subscription event.
// Note that watchman will deliver a list of all current files
// when you first subscribe, so you don't need to walk the tree
// for yourself on startup
client.on('subscription', function(resp) {
console.log(resp.root, resp.subscription, resp.files);
});
To cancel a subscription, use the unsubscribe
command and pass in the name of
the subscription you want to cancel:
client.command(['unsubscribe', process.cwd(), 'mysubscription']);
Note that subscriptions names are scoped to your connection to the watchman service; multiple different clients can use the same subscription name without fear of colliding.