large-watcher
Version:
A watcher for NodeJS, that works well with large directories
239 lines (193 loc) • 6.22 kB
JavaScript
// Requires
var inherits = require("util").inherits;
var EventEmitter = require("events").EventEmitter;
// Utility function wrapping find command
var find = require('./find');
function Watcher(dirname, period, filter) {
if(!(this instanceof Watcher)) {
return new Watcher(dirname, period);
}
// Polling period
this.period = period;
// Directy to monitor
this.dirname = dirname;
// File filter
this.filter = filter || defaultFilter;
// Intervals to track
this.dumpTimeout = null;
this.modifiedTimeout = null;
// Bind poll methods
this.pollDump = this.pollDump.bind(this);
this.pollModified = this.pollModified.bind(this);
// Bind handler methods
this.dumpHandler = this.dumpHandler.bind(this);
this.deletedHandler = this.deletedHandler.bind(this);
this.createdHandler = this.createdHandler.bind(this);
this.modifiedHandler = this.modifiedHandler.bind(this);
// Buffered data
this.buffer = {
/*
created: [],
deleted: [],
modified: [],
*/
};
// Previous dumped tree
this.prevTree = null;
// Stopped state
this.stopped = true;
Watcher.super_.call(this);
}
inherits(Watcher, EventEmitter);
Watcher.prototype.start = function() {
this.stopped = false;
return this.poll();
};
Watcher.prototype.poll = function() {
// Watcher is stopped, no longer poll
if(this.stopped) {
return this;
}
this.dumpTimeout = this.pollDump(this.dumpHandler);
this.modifiedTimeout = this.pollModified(this.modifiedHandler);
return this;
};
Watcher.prototype.stop = function() {
this.stopped = true;
clearTimeout(this.dumpTimeout);
clearTimeout(this.modifiedTimeout);
return this;
};
Watcher.prototype.cleanup = function() {
return this.stop()
.removeAllListeners('change')
.removeAllListeners('created')
.removeAllListeners('deleted')
.removeAllListeners('modified');
};
Watcher.prototype.handle = function(type, files) {
// Set changes to buffer
this.buffer[type] = files;
// Adjust buffer, apply corrections
// 1. Remove created files from modified files
if(this.buffer.created && this.buffer.modified) {
this.buffer.modified = arrayDiff(this.buffer.modified, this.buffer.created);
}
// Ready to flush ?
if(!(
this.buffer.deleted &&
this.buffer.created &&
this.buffer.modified
)) {
return;
}
// Do we have data to flush ?
if (
this.buffer.created.length > 0 ||
this.buffer.deleted.length > 0 ||
this.buffer.modified.length > 0
) {
// Flush buffer
this.emit('change', this.buffer);
// This code could be more generic
// But I've kept as such for simplicity and readibility
// Check if individual changes needed emitted
if(this.buffer.created.length > 0) {
this.emit('created', this.buffer.created);
}
if(this.buffer.deleted.length > 0) {
this.emit('deleted', this.buffer.deleted);
}
if(this.buffer.modified.length > 0) {
this.emit('modified', this.buffer.modified);
}
}
// Clear buffer
this.buffer = {};
// Now start polling again
this.poll();
};
Watcher.prototype.dumpHandler = function(err, diffs) {
this.deletedHandler(err, err ? null : diffs[0]);
this.createdHandler(err, err ? null : diffs[1]);
};
Watcher.prototype.modifiedHandler = function(err, files) {
if(err) {
return this.emit('error', err);
}
return this.handle('modified', files);
};
Watcher.prototype.createdHandler = function(err, files) {
if(err) {
return this.emit('error', err);
}
return this.handle('created', files);
};
Watcher.prototype.deletedHandler = function(err, files) {
if(err) {
return this.emit('error', err);
}
return this.handle('deleted', files);
};
Watcher.prototype.pollModified = function(cb) {
var that = this;
var shouldPrune = true;
return setTimeout(function() {
find.modifiedSince(that.dirname, 1, shouldPrune, function(err, files) {
cb(err, files.filter(that.filter));
});
}, this.period * 1000);
};
Watcher.prototype.pollDump = function(cb) {
var that = this;
// Should be prune common unimportant folders ?
// Pruning is forced by default
// TODO: provide option to deactivate
var shouldPrune = true;
// Poll
return setTimeout(function() {
find.dump(that.dirname, shouldPrune, function(err, files) {
var tree = files.filter(that.filter);
if(err) {
return cb(err, []);
} else if(!that.prevTree) {
// Get first initial tree
that.prevTree = tree;
// Continue
that.pollDump(cb);
return;
}
// Get deleted and created files
var d = dualDiff(that.prevTree, tree);
// Retun data
cb(null, d);
// Make current tree the previous
that.prevTree = tree;
});
}, this.period/2 * 1000);
};
function defaultFilter(filepath) {
// Ignore files/folders starting with "."
return filepath.match(/(^\.)|(\/\.)|(\\\.)/) === null;
}
// Utility diff function
// TODO: improve speed
function arrayDiff(a1, a2) {
var o1={}, o2={}, diff=[], i, len, k;
for (i=0, len=a1.length; i<len; i++) { o1[a1[i]] = true; }
for (i=0, len=a2.length; i<len; i++) { o2[a2[i]] = true; }
for (k in o1) { if (!(k in o2)) { diff.push(k); } }
//for (k in o2) { if (!(k in o1)) { diff.push(k); } }
return diff;
}
// Returns both the left and right diff seperately
function dualDiff(a1, a2) {
var o1={}, o2={}, diff1=[], diff2=[], i, len, k;
for (i=0, len=a1.length; i<len; i++) { o1[a1[i]] = true; }
for (i=0, len=a2.length; i<len; i++) { o2[a2[i]] = true; }
for (k in o1) { if (!(k in o2)) { diff1.push(k); } }
for (k in o2) { if (!(k in o1)) { diff2.push(k); } }
return [diff1, diff2];
}
// Exports
module.exports = Watcher;