miteru
Version:
simple file watcher
1,207 lines (983 loc) • 37.4 kB
JavaScript
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.miteru = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
;
function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
// var rimraf = require('rimfraf')
// rimraf.sync('tmp')
var fs = require('fs');
var path = require('path');
var minimatch = require('minimatch');
var ALWAYS_COMPARE_FILECONTENT = false;
var ATTEMPT_INTERVAL = 25; // milliseconds
var MAX_ATTEMPTS = 5;
var MAX_READ_FILE_SYNC_ATTEMPTS = 2;
var START_TIME = Date.now();
var TRIGGER_DELAY = 1;
var glob = require('redstar'); // some file systems round up to the nearest full second (e.g. OSX)
// for file mtime, atime, ctime etc -- so in order to account for
// this edge case ( very edgy ) we need to diff the file contents
// to determine if the file has been changed during this time
var EDGE_CASE_INTERVAL = 1000; // milliseconds
// diffing HUGE files hurts the soul so we cap it at a reasonable (TM) size..
var EDGE_CASE_MAX_SIZE = 1024 * 1024 * 10; // 15mb
// polling interval intensities based on delta time ( time since last change )
var TEMPERATURE = {
HOT: {
AGE: 1000 * 60 * 1,
// 1 min
INTERVAL: 50
},
SEMI_HOT: {
AGE: 1000 * 60 * 15,
// 15 min
INTERVAL: 88
},
WARM: {
AGE: 1000 * 60 * 60 * 4,
// 4 hours
INTERVAL: 200
},
COLD: {
AGE: 1000 * 60 * 60 * 24,
// 24 hours
INTERVAL: 333
},
COLDEST: {
INTERVAL: 625
},
DORMANT: {
INTERVAL: 200
}
};
var DEBUG = {
FILE: false,
TEMPERATURE: false,
ENOENT: false,
LOG: false,
EVT: true,
DEV: false
};
function debugLog(type, msg) {
if (DEBUG[type.toUpperCase()]) {
console.log(msg);
}
} // set verbosity based on getEnv variable MITERU_LOGLEVEL
switch ((process.env.MITERU_LOGLEVEL || '').toLowerCase()) {
case 'silent':
case 'silence':
case 'quiet':
case 'nolog':
case 'nologging':
DEBUG = {};
break;
case 'temp':
case 'temperature':
DEBUG = {
TEMPERATURE: true
};
break;
case 'full':
case 'all':
DEBUG = {
TEMPERATURE: true,
ENOENT: true,
FILE: true,
LOG: true,
EVT: true
};
break;
case 'evt':
case 'evts':
case 'event':
case 'events':
DEBUG = {
EVT: true
};
break;
case 'dev':
DEBUG.DEV = true;
break;
default:
DEBUG = {};
break;
} // DEBUG.TEMPERATURE = true
function getEnv(key) {
var v = process.env[key];
if (v == null) return false;
try {
return !!JSON.parse(v);
} catch (err) {
return false;
}
}
var _running = true; // list of all files being watched ( and continuously polled )
var _fileWatchers = {}; // list of the most active files being watched ( and are thus
// prioritised for polling ( faster polling ) )
var _activeList = []; // cleanup
process.on('exit', function () {
_running = false;
Object.keys(_fileWatchers).forEach(function (key) {
var fw = _fileWatchers[key];
fw && fw.close();
});
});
var api = module.exports = {};
api.getWatched = function getWatched() {
// TODO caching? premature optimization?
// JavaScript doesn't guarantee ordering here so we sort
// it alphabetically for consistency
return Object.keys(_fileWatchers).sort();
};
api.getPollingInterval = function getPollingInterval(file) {
var filepath = path.resolve(file);
var fw = _fileWatchers[filepath];
return fw && fw.pollInterval;
}; // return internal fw object
// TODO check pollInterval and temperature
api._getFileWatcher = function _getFileWatcher(file) {
var filepath = path.resolve(file);
return _fileWatchers[filepath];
};
api._activeList = _activeList;
api.reset = function reset() {
Object.keys(_fileWatchers).forEach(function (key) {
var fw = _fileWatchers[key];
fw && fw.close();
delete _fileWatchers[key];
});
_activeList.length = 0;
api._NOEXISTS_SLEEP_DELAY = 1000 * 15;
api._NOEXIST_INTERVAL = 400; // special polling interval for files that do not exist
api._MAX_ACTIVE_LIST_LENGTH = 6;
api._CPU_SMOOTHING_DELAY = 3000; // milliseconds
api._stats = {
report: !!getEnv('MITERU_STATS'),
cpu: 45,
cpus: [45, 45, 45],
pollCounter: 0,
maxPollTime: 0,
minPollTime: 999999,
// extra polling time based on number of files watched
extraTime: 100
};
api.options = {
// minimum polling interval per file
minInterval: undefined
};
delete api._disableCpuSmoothing;
};
api.reset();
var _watcherIds = 1;
function selectFirstOfType(items, type) {
for (var i = 0; i < items.length; i++) {
var item = items[i];
var isArray = Array.isArray(item);
var itemType = isArray ? 'array' : _typeof(item);
if (itemType === type) {
// eslint-disable-line
items.splice(0, i + 1); // mutates array
return item;
}
}
return undefined;
}
api.watch = function watch(file, opts, callback) {
var args = [file, opts, callback].filter(function (item) {
return !!item;
}).reverse();
callback = selectFirstOfType(args, 'function');
opts = selectFirstOfType(args, 'object');
file = args[args.length - 1]; // this object is returned by this function
var watcher = {
id: _watcherIds++,
opts: opts,
files: {},
callback: callback,
evtCallbacks: {},
fileCounter: 0
}; // set initFlagged for files added to the watcher within the same tick (event loop) (files added on/during init).
var _initFlagged = true;
setTimeout(function () {
// turn the flag off for all other files added after the first tick (event loop) (on/during init).
_initFlagged = false;
}, 0);
watcher.add = function add(file) {
if (!file) return;
if (file instanceof Array) {
return file.forEach(function (f) {
watcher.add(f);
});
}
if (typeof file !== 'string') {
throw new Error('file was not a string');
}
var isPattern = glob.hasMagic(file); // scope _initFlagged for these files
var initFlagged = _initFlagged;
if (isPattern) {
// is glob pattern for zero or multiple files
var pattern = file;
glob(pattern, {
ignore: ['node_modules']
}, function (err, files) {
if (err) {
if (!(err instanceof Array)) err = [err];
err.forEach(function (e) {
console.error(e && e.err && e.err.message);
throw e;
});
} // console.log( 'glob finished' )
files.forEach(function (file) {
// console.log( 'file: ' + file )
// ignore patterns matching node_modules files
if (file.indexOf('node_modules') === -1) {
watchFile(watcher, file, initFlagged);
}
});
});
} else {
// is a single file path
watchFile(watcher, file, initFlagged);
}
return watcher; // chaining
};
watcher.on = function on(evt, callback) {
watcher.evtCallbacks[evt] = watcher.evtCallbacks[evt] || [];
watcher.evtCallbacks[evt].push(callback); // return off function
return function off() {
var i = watcher.evtCallbacks[evt].indexOf(callback);
if (i !== -1) {
return watcher.evtCallbacks[evt].splice(i, 1);
}
};
};
watcher._setDebugFlag = function _setDebugFlag(file, key, value) {
var filepath = path.resolve(file);
var fw = watcher.files[filepath];
if (!fw) {
throw new Error('no fileWatcher for [$1] found'.replace('$1', filepath));
} else {
fw._debug[key] = value;
}
};
watcher.unwatch = function unwatch(file) {
var isPattern = glob.hasMagic(file);
if (isPattern) {
// is glob pattern for zero or multiple files
var pattern = file;
var files = Object.keys(watcher.files);
files.forEach(function (file) {
var shouldRemove = minimatch(file, pattern);
if (shouldRemove) {
unwatchFile(watcher, file);
}
});
} else {
// is a single file path
unwatchFile(watcher, file);
}
return watcher; // chaining
};
watcher.getWatched = function getWatched() {
// TODO caching? premature optimization?
// JavaScript doesn't guarantee ordering here so we sort
// it alphabetically for consistency
return Object.keys(watcher.files).sort();
};
watcher.getLog = function getLog(file) {
var filepath = path.resolve(file);
var fw = watcher.files[filepath];
var o = {};
if (!fw) return undefined;
Object.keys(fw.log).forEach(function (key) {
o[key] = fw.log[key];
});
return o;
};
watcher.close = function close() {
Object.keys(watcher.files).forEach(function (filepath) {
unwatchFile(watcher, filepath);
});
watcher.add = function () {
throw new Error('watcher has been closed.');
};
watcher.watch = function () {
throw new Error('watcher has been closed.');
}; // JavaScript functions return 'undefined' by default -- but
// explicitly writing it here as it is intended
// behaviour because after a watcher is closed it stays closed.
// And attempting to chain it is, and is supposed to be, an error.
return undefined; // ( default behavour ) ( no chaining )
}; // helper function
watcher.clear = function clear() {
watcher.unwatch('**');
if (watcher.getWatched().length !== 0) {
throw new Error('a clear attempt did not remove all watched files!');
}
return watcher; // chaining
};
if (file != null) {
watcher.add(file);
}
return watcher;
};
function statsFunction() {
statsFunction.reset = function reset() {
clearTimeout(statsFunction.timeout);
statsFunction.timeout = undefined;
statsFunction.time = Date.now();
statsFunction._lastCpuUsage = {};
statsFunction._lastCpuUsage = process.cpuUsage();
statsFunction._lastCpuUsageTime = Date.now();
};
statsFunction.reset();
function usage() {
var cpuUsage = process.cpuUsage();
var now = Date.now();
var prevTotal = statsFunction._lastCpuUsage.user + statsFunction._lastCpuUsage.system;
var total = cpuUsage.user + cpuUsage.system;
var diff = total - prevTotal + 0.01;
var delta = now - statsFunction._lastCpuUsageTime;
var limit = delta * 1000 + 0.1; // microseconds to milliseconds
var pct = String(100 * (diff / limit)).trim().slice(0, 6);
statsFunction._lastCpuUsage = cpuUsage;
statsFunction._lastCpuUsageTime = now;
return pct;
}
statsFunction.start = function () {
if (statsFunction.timeout === undefined) {
clearTimeout(statsFunction.timeout);
statsFunction.timeout = setTimeout(statsTick, 1000);
}
};
statsFunction.time = Date.now();
function statsTick() {
if (Object.keys(_fileWatchers).length > 0) {
var now = Date.now();
var delta = now - statsFunction.time;
if (delta >= 1000) {
statsFunction.time = now;
var _stats = api._stats;
_stats.cpus.push(usage());
while (_stats.cpus.length > 3) {
_stats.cpus.shift();
}
var sum = 0;
_stats.cpus.forEach(function (cpu) {
sum += Number(cpu);
});
_stats.cpu = Math.round(sum / _stats.cpus.length);
var fileCount = Object.keys(_fileWatchers).length;
_stats.extraTime = Math.round(Math.pow(fileCount / 75, 1.45));
if (_stats.report) {
console.log('[miteru]: files: ' + fileCount);
console.log('[miteru]: cpu usage: ' + _stats.cpu);
console.log('[miteru]: poll counter: ' + _stats.pollCounter);
console.log('[miteru]: max poll time: ' + _stats.maxPollTime);
console.log('[miteru]: min poll time: ' + _stats.minPollTime);
console.log('[miteru]: extra time: ' + _stats.extraTime);
console.log('[miteru]: active files: ' + _activeList.length);
if (getEnv('MITERU_PROMOTION_LIST')) {
_activeList.forEach(function (fw) {
console.log('mtime: ' + new Date(fw.mtime).toLocaleString() + ' , filepath: ' + path.relative(process.cwd(), fw.filepath));
});
}
}
_stats.pollCounter = 0;
_stats.maxPollTime = 0;
_stats.minPollTime = 999999;
}
clearTimeout(statsFunction.timeout);
statsFunction.timeout = setTimeout(statsTick, 1000 - delta);
}
}
}
statsFunction();
api.minimatch = minimatch;
function watchFile(watcher, file, initFlagged) {
var filepath = path.resolve(file);
var fw = _fileWatchers[filepath];
if (fw) {
// already watching
debugLog('log', 'file already being globally watched');
} else {
// add new file watcher
fw = createFileWatcher(filepath);
_fileWatchers[filepath] = fw;
statsFunction.start();
}
if (watcher.files[filepath] === fw) {
debugLog('log', '(ignored) local watcher already watching that file');
return;
}
watcher.files[filepath] = fw;
fw.watchers[watcher.id] = watcher;
watcher.fileCounter++; // initFlagged indicates that this file was added to the watch list
// during the same tick (nodejs process tick)
// TODO handle initFlagged separately for each local watcher?
if (initFlagged === true) {
fw.initFlagged = true;
} // since we have not polled yet we do not know if the
// file actually exists on the disk already (usually it
// does)
fw.exists = false;
fw._promoteOnNextStat = true;
}
function unwatchFile(watcher, file) {
var filepath = path.resolve(file);
var fw = _fileWatchers[filepath];
var wfw = watcher.files[filepath];
if (!fw && !wfw) {
// already unwatched
debugLog('log', '(ignored) file already unwatched');
return;
}
if (wfw) {
delete watcher.files[filepath];
watcher.fileCounter--;
}
if (fw) {
delete fw.watchers[watcher.id];
if (isFileWatcherEmpty(fw)) {
// close fileWatcher since nobody is watching it anymore
fw.close();
}
}
if (Object.keys(_fileWatchers).length === 0) {
clearTimeout(statsFunction.timeout);
statsFunction.timeout = undefined;
}
}
function isFileWatcherEmpty(fw) {
return Object.keys(fw.watchers).length <= 0;
}
function createFileWatcher(filepath) {
// console.log( 'creating fileWatcher: ' + filepath )
filepath = path.resolve(filepath);
var fw = {
filepath: filepath,
watchers: {},
log: {},
_debug: {},
readFileSyncAttempts: 0,
attempts: 0
};
fw.close = function () {
clearTimeout(fw.timeout); // clearTimeout( fw.fileContentTimeout )
fw.closed = true;
Object.keys(fw.watchers).forEach(function (key) {
var watcher = fw.watchers[key]; // clear the file from the watch list of the watchers
delete watcher.files[fw.filepath];
});
delete _fileWatchers[fw.filepath];
}; // start polling the filepath
schedulePoll(fw, 1);
return fw;
}
function unlockFile(fw) {
if (fw.locked !== true) throw new Error('fw was not locked when attempting to unlock');
fw.locked = false;
}
function schedulePoll(fw, forcedInterval) {
if (fw.closed) throw new Error('fw is closed');
if (fw.locked) throw new Error('fw locked');
var interval = fw.pollInterval || 100;
if (fw.exists !== true) {
var now = Date.now();
if (fw._noExistsTime) {
var delta = now - fw._noExistsTime; // slow down polling for nonexistent files
// that aren't hot
if (delta > api._NOEXISTS_SLEEP_DELAY) {
if (interval < api._NOEXIST_INTERVAL) {
interval = api._NOEXIST_INTERVAL;
}
}
} else {
fw._noExistsTime = now;
}
} else {
delete fw._noExistsTime;
}
var opts = api.options;
if (opts.minInterval && interval < opts.minInterval) {
interval = opts.minInterval;
}
if (forcedInterval != null) interval = forcedInterval; // event is ready to be fired, FIRE FIRE FIRE!!! :D
if (fw._eventReadyToFire) {
// console.log( ' events are ready to fire, polling ASAP!' )
interval = 1; // poll ASAP ( if the next poll is unchaged aka "stable", we fire the pending event
}
if (fw.timeout !== undefined) throw new Error('fw.timeout already in progress');
clearTimeout(fw.timeout);
fw.timeout = setTimeout(function () {
fw.timeout = undefined;
fw._lastPollInterval = interval;
pollFile(fw);
}, interval);
}
function pollFile(fw) {
if (!_running) return undefined;
if (fw.closed) throw new Error('fw is closed');
if (fw.locked) throw new Error('fw is locked');
fw.locked = true;
debugLog('dev', ' == fs.stat:ing == '); // var isEdgy = ( fw.mtime && ( Date.now() - stats.mtime ) < EDGE_CASE_INTERVAL )
// TODO rethink fs.readFileSync situation
// ( could be that fs.stat is outdated when fs.readFileSync is performed )
if (fw._debug.removeFileWatcherDuringFSStat) {
fw._debug.removeFileWatcherDuringFSStat = false;
Object.keys(fw.watchers).forEach(function (key) {
var watcher = fw.watchers[key];
watcher.unwatch(fw.filepath);
});
}
fs.stat(fw.filepath, function (err, stats) {
if (fw.closed || _fileWatchers[fw.filepath] !== fw) {
debugLog('log', 'fw has been closed');
return undefined;
}
if (fw.locked !== true) throw new Error('fw was not locked prior to fs.stat'); // TODO stats
var now = Date.now();
if (fw._lastPollTime) {
var delta = now - fw._lastPollTime;
var _stats = api._stats;
var _wstats = _stats;
_wstats.pollCounter++; // TODO keep track of max and min from currently watched files, not
// historical max/min
if (delta > _wstats.maxPollTime) _wstats.maxPollTime = delta;
if (delta < _wstats.minPollTime) _wstats.minPollTime = delta;
}
fw._lastPollTime = now;
if (err) {
switch (err.code) {
case 'ENOENT':
case 'EACCES':
case 'EPERM':
debugLog('enoent', ' === POLL ENOENT === ');
handleFSStatError(fw);
break;
default:
throw err;
}
} else {
// no error
if (stats.size <= 0 && fw.size !== stats.size) {
debugLog('dev', ' ============ size was falsy: ' + stats.size);
if (fw.attempts < MAX_ATTEMPTS) {
// handle as a potential ENOENT error, i.e., increment
// error counter but this event alone cannot consider the file
// non-existent -- it's a good indicator that the file is unstable
// and might not exist soon
fw.attempts++; // schedule next poll faster than normal ( ATTEMPT_INTERVAL )
unlockFile(fw);
return schedulePoll(fw, ATTEMPT_INTERVAL);
} else {
// if we've exceeded attempts then assume the file exists
// and it's intentionally empty ( of size 0 )
debugLog('dev', ' consider file empty ');
}
}
debugLog('dev', ' == fs.stat OK == '); // debugging helpers
if (fw._debug.removeAfterFSStat) {
fw._debug.removeAfterFSStat = false; // related test:
// 'watch a new file after init removed between FSStat:ing'
fs.unlinkSync(fw.filepath);
}
if (fw._debug.chmodAfterFSStat) {
fw._debug.chmodAfterFSStat = false; // related test:
// 'watch a new file after init removed between FSStat:ing'
fs.chmodSync(fw.filepath, 0
/* no permission */
);
}
if (fw._debug.changeContentAfterFSStat) {
fw._debug.changeContentAfterFSStat = false; // related test:
// 'watch a single file -- file content appended between FSStat:ing'
var text = fs.readFileSync(fw.filepath).toString('utf8'); // console.log( 'text was: ' + text )
text += ' + "-FSStatDebug"';
fs.writeFileSync(fw.filepath, text);
debugLog('dev', 'written: ' + text);
}
fw.attempts = 0; // reset attempts
var type = 'unknown';
if (stats.isFile()) {
type = 'file';
} else if (stats.isDirectory()) {
type = 'directory';
}
if (type !== 'file') {
var message = 'only filetype of "file" is supported, found filetype [ ' + type + ' ]'; // emit to watchers
Object.keys(fw.watchers).forEach(function (key) {
var watcher = fw.watchers[key];
var evt = 'error';
if (typeof watcher.callback === 'function') {
watcher.callback(evt, fw.filepath, message);
}
var evtCallbacks = watcher.evtCallbacks[evt] || [];
evtCallbacks.forEach(function (evtCallback) {
return evtCallback(fw.filepath, message);
}); // delete the file from being watched anymore
delete watcher.files[fw.filepath]; // delete the watcher reference from the fw
delete fw.watchers[key];
});
return fw.close(); // stop polling the directory
}
var existedPreviously = fw.exists === true; // size change or mtime increase are good indicators that the
// file has been modified*
//
// *not a 100% guarantee -- for example the file content may not have changed
// from the previous file content -- however, we do not care if the file content
// has not changed and we will avoid comparing/reading the file contents unless
// necessary -- for example during EDGE_CASE_INTERVAL we will have to check
// file contents -- or perhaps when a flag is set? ( ALWAYS_COMPARE_FILECONTENT? )
// TODO
var sizeChanged = stats.size !== fw.size;
var mtimeChanged = stats.mtime > fw.mtime;
var skipEdgeCase = stats.size >= EDGE_CASE_MAX_SIZE;
var isEdgy = Date.now() - stats.mtime < EDGE_CASE_INTERVAL;
isEdgy && debugLog('dev', 'is edgy');
var shouldCompareFileContents = ALWAYS_COMPARE_FILECONTENT || // during edge case period we can't rely on mtime or file size alone
// and we need to compare the actual contents of the file
isEdgy && !skipEdgeCase;
var fileContentHasChanged;
var shouldReadFileContents = !existedPreviously || sizeChanged || mtimeChanged || shouldCompareFileContents;
if (fw.readFileSyncAttempts >= MAX_READ_FILE_SYNC_ATTEMPTS) {
shouldReadFileContents = false;
}
if (shouldReadFileContents) {
debugLog('file', 'FILE WILL READ CONTENT : ' + fw.filepath);
}
if (shouldCompareFileContents) {
debugLog('file', 'FILE WILL COMPARE CONTENT : ' + fw.filepath);
}
var fileContent; // only fs.readFileSync fileContent if necessary
if (shouldReadFileContents) {
try {
// NOTE
// there's a caveat here that fs.stat and fs.readFileSync
// may be out of sync -- in other words fs.readFileSync may return
// an updated file content that may not be reflected by the
// size and mtime reported by the fs.stat results that came before it
// -> this may result in two change events being emitted if left as is
// -> in order to combat this, if we will update the mtime and size
// to the most recent values if we detect that the file contents
// have been updated [#1]
fileContent = fs.readFileSync(fw.filepath);
debugLog('dev', 'read file contents: ' + fileContent.toString('utf8'));
} catch (err) {
switch (err.code) {
case 'EACCES':
case 'ENOENT':
case 'EPERM':
fw.attempts++;
fw.readFileSyncAttempts++; // possibly if file is removed between a succesful fs.stat
// and fs.readFileSync
// -- simply let pollFile handle it ( and any errors ) again
// -- if the file has really been removed then the next fs.stat will
// be able to handle it
// console.log( 'removed between fs.stat and fs.readFileSync' )
fw.log['FSStatReadFileSyncErrors'] = (fw.log['FSStatReadFileSyncErrors'] || 0) + 1;
process.nextTick(function () {
unlockFile(fw);
schedulePoll(fw, ATTEMPT_INTERVAL); // pollFile( fw )
});
return undefined;
default:
console.error('Error between fs.stat and fs.readFileSync');
throw err;
}
}
}
fileContentHasChanged = existedPreviously && fileContent && fw.fileContent && !fw.fileContent.equals(fileContent) // !existedPreviously || (
// fileContent &&
// fw.fileContent &&
// !( fw.fileContent.equals( fileContent ) )
// )
;
debugLog('dev', ' == 1 == ');
if (fileContentHasChanged) {
debugLog('dev', 'content was: ' + fw.fileContent.toString('utf8'));
debugLog('dev', 'content now: ' + fileContent.toString('utf8'));
}
debugLog('dev', ' == 2 == ');
if (sizeChanged) {
debugLog('dev', 'size was: ' + fw.size);
debugLog('dev', 'size now: ' + stats.size);
}
debugLog('dev', ' == 3 == ');
if (mtimeChanged) {
debugLog('dev', 'mtime was: ' + fw.mtime);
debugLog('dev', 'mtime now: ' + stats.mtime);
}
debugLog('dev', ' == 4 == '); // update fileContent if necessary
if (fileContent && (!existedPreviously || fileContentHasChanged)) {
debugLog('dev', 'fileContent updated: ' + fileContent.toString('utf8')); // console.log( fileContent.toString( 'utf8' ) )
setFileContent(fw, fileContent);
}
debugLog('dev', ' == 5 == ');
if (fileContentHasChanged) {
// [#1]
// update mtime and size to homogenize the
// potential diffs between calls to fs.stat and fs.readFileSync
//
// Another solution could be to debounce or throttle
// event triggering
//
// related test:
// 'watch a single file -- file content appended between FSStat:ing'
//
// Another solution could be to test during stats.size or stats.mtime
// change if fileContent equals to fw.fileContent then skip
// triggering a 'change' event
var newMtime = Date.now();
if (stats.mtime < newMtime) {
debugLog('dev', ' updated stats.mtime');
stats.mtime = new Date(newMtime);
}
if (stats.size !== fileContent.length) {
debugLog('dev', ' updated stats.size -- was: $was, is: $is'.replace('$was', stats.size).replace('$is', fileContent.length));
stats.size = fileContent.length;
}
}
debugLog('dev', ' == 6 == '); // update stats
fw.type = type;
fw.exists = true;
fw.size = stats.size;
fw.mtime = stats.mtime;
if (!isEdgy) fw.readFileSyncAttempts = 0; // reset attempts
if (fw._promoteOnNextStat && fw.mtime) {
fw._promoteOnNextStat = false;
promote(fw);
}
fw._stats = stats; // remember stats object
debugLog('dev', ' == 7 == '); // change the polling interval dynamically
// based on how often the file is changed
updatePollingInterval(fw);
debugLog('dev', ' == 8 == '); // schedule next poll
unlockFile(fw);
schedulePoll(fw);
debugLog('dev', ' == 9 == '); // trigger events
if (existedPreviously) {
debugLog('dev', ' == 10 == ');
if (sizeChanged || mtimeChanged || fileContentHasChanged) {
debugLog('evt', 'change: ' + fw.filepath);
debugLog('dev', 'change evt -- sizeChanged $1, mtimeChanged $2, fileContentHasChanged $3: [$4]'.replace('$1', sizeChanged).replace('$2', mtimeChanged).replace('$3', fileContentHasChanged).replace('$4', fw.filepath));
loadEvent(fw, 'change');
} else {
// file is now stable ( two consecutive polls have
// found no changes )
// fire away events ( add, change ) when file is stable
if (!fw._debug.keepUnstable) {
dispatchPendingEvent(fw);
}
}
debugLog('dev', ' == 11 == ');
} else {
debugLog('dev', ' == 12 == ');
if (fw.initFlagged === true) {
fw.initFlagged = false;
debugLog('evt', 'init: ' + fw.filepath);
debugLog('dev', 'init evt -- sizeChanged $1, mtimeChanged $2, fileContentHasChanged $3: [$4]'.replace('$1', sizeChanged).replace('$2', mtimeChanged).replace('$3', fileContentHasChanged).replace('$4', fw.filepath));
loadEvent(fw, 'init');
dispatchPendingEvent(fw); // init is safe to fire straight away
} else {
debugLog('evt', 'add: ' + fw.filepath);
debugLog('dev', 'add evt -- sizeChanged $1, mtimeChanged $2, fileContentHasChanged $3: [$4]'.replace('$1', sizeChanged).replace('$2', mtimeChanged).replace('$3', fileContentHasChanged).replace('$4', fw.filepath));
loadEvent(fw, 'add');
}
debugLog('dev', ' == 13 == ');
}
}
});
}
function handleFSStatError(fw) {
var existedPreviously = fw.exists === true;
fw.attempts++;
var fileShouldExist = existedPreviously || fw.initFlagged;
if (fileShouldExist) {
// file existed previously, assume that it should still
// exist and attempt to fs.stat it again.
// or if the file was added on init -- assume that it should
// exist ( or will exist within a few milliseconds [*] )
// [*] within (MAX_ATTEMPTS * ATTEMPT_INTERVAL) milliseconds
// MAX_ATTEMPTS exceeded and not uncertain error event type
if (fw.attempts > MAX_ATTEMPTS) {
// after a number of failed attempts
// consider the file truly non-existent
fw.exists = false;
if (fw.initFlagged) {
// TODO -- throw error since file on init didn't exist?
// perhaps user expects it to exist since it was added on init?
// --
// in any case the init phase has ended
// for this file did it exist or not
fw.initFlagged = false;
} // TODO trigger 'unlink' event
debugLog('evt', 'unlink: ' + fw.filepath);
debugLog('dev', 'unlink evt -- [$4]'.replace('$4', fw.filepath));
loadEvent(fw, 'unlink'); // schedule next poll
unlockFile(fw);
schedulePoll(fw);
} else {
// schedule next poll faster than normal ( ATTEMPT_INTERVAL )
unlockFile(fw);
schedulePoll(fw, ATTEMPT_INTERVAL);
}
} else {
// in any case the init phase has ended
// for this file did it exist or not
fw.initFlagged = false; // fire away events ( unlink ) when file is stable
dispatchPendingEvent(fw); // file didn't exist previously so it's safe to assume
// it still doesn't and isn't supposed to exist
// schedule next poll normally
unlockFile(fw);
schedulePoll(fw);
}
}
function dispatchPendingEvent(fw) {
if (fw._eventReadyToFire) {
var evt = fw._eventReadyToFire;
fw._eventReadyToFire = undefined;
clearTimeout(fw.triggerTimeout);
fw.triggerTimeout = setTimeout(function () {
if (evt !== 'init') fw._awake = true;
if (evt === 'init' || evt === 'add' || evt === 'change') {
promote(fw);
} // emit to watchers
Object.keys(fw.watchers).forEach(function (key) {
var watcher = fw.watchers[key];
if (typeof watcher.callback === 'function') {
watcher.callback(evt, fw.filepath, fw._stats);
}
var evtCallbacks = watcher.evtCallbacks[evt] || [];
evtCallbacks.forEach(function (evtCallback) {
return evtCallback(fw.filepath, fw._stats);
});
});
}, TRIGGER_DELAY);
}
}
function loadEvent(fw, evt) {
// do not overwrite with 'change' events
if (fw._eventReadyToFire && evt === 'change') return undefined; // 'add' and 'unlink' cancels each other out ( rare* )
// *this can happen if a file is being written without being stable
// and is then deleted ( since events are dispatched AFTER the file is stable )
// and by stable we mean the file has not changed between two polls ( ~10 milliseconds )
if (fw._eventReadyToFire === 'add' && evt === 'unlink') {
// console.log( ' == GIRAFFE == ' )
fw._eventReadyToFire = undefined;
fw.log['loadEventsAbortedCount'] = (fw.log['loadEventsAbortedCount'] || 0) + 1;
return undefined;
}
if (fw._eventReadyToFire) {
debugLog('dev', 'unfired triggered overriden with new [$1] -> [$2]'.replace('$1', fw._eventReadyToFire).replace('$2', evt));
}
fw._eventReadyToFire = evt;
}
function setFileContent(fw, content) {
fw.fileContent = content; // clear fileContent once EDGE_CASE_INTERVAL is no longer relevant
clearTimeout(fw.fileContentTimeout);
fw.fileContentTimeout = setTimeout(function () {
delete fw.fileContent;
}, EDGE_CASE_INTERVAL);
} // function updateStats ( fw, stats ) {
// fw.size = stats.size
// fw.mtime = stats.mtime
// }
// keep track of the top ( _MAX_ACTIVE_LIST_LENGTH ) actively changing files
// by setting their active flag thus prioritizing their polling
function promote(fw) {
if (fw.active) return;
var list = _activeList;
var shouldSort = false;
var maxActiveListLength = api._MAX_ACTIVE_LIST_LENGTH;
if (list.length < maxActiveListLength) {
fw.active = true;
list.push(fw);
shouldSort = true;
} else {
// only need to compare with the last item because it's
// pre-sorted
var lastItem = list[list.length - 1];
var isMoreRecentlyModified = lastItem && fw.mtime > lastItem.mtime;
if (!lastItem || isMoreRecentlyModified) {
fw.active = true;
list.push(fw);
shouldSort = true;
}
}
if (shouldSort) {
list.sort(function (a, b) {
return b.mtime - a.mtime;
});
} // trim and deactivate overflowing files
while (list.length > maxActiveListLength) {
var pop = list.pop();
pop.active = false;
}
}
function updatePollingInterval(fw) {
var filepath = fw.filepath;
if (fw.type !== 'file') {
fw.pollInterval = 3000;
return;
}
var now = Date.now();
var delta = now - fw.mtime;
if (fw) {
if (delta < TEMPERATURE.HOT.AGE) {
if (fw.pollInterval !== TEMPERATURE.HOT.INTERVAL) {
debugLog('temperature', 'HOT file: ' + filepath);
fw.temperature = 'hot';
fw.pollInterval = TEMPERATURE.HOT.INTERVAL;
}
} else if (delta < TEMPERATURE.SEMI_HOT.AGE) {
if (fw.pollInterval !== TEMPERATURE.SEMI_HOT.INTERVAL) {
debugLog('temperature', 'SEMI_HOT file: ' + filepath);
fw.temperature = 'semi_hot';
fw.pollInterval = TEMPERATURE.SEMI_HOT.INTERVAL;
}
} else if (delta < TEMPERATURE.WARM.AGE) {
if (fw.pollInterval !== TEMPERATURE.WARM.INTERVAL) {
debugLog('temperature', 'WARM file: ' + filepath);
fw.temperature = 'warm';
fw.pollInterval = TEMPERATURE.WARM.INTERVAL;
}
} else if (delta < TEMPERATURE.COLD.AGE) {
if (fw.pollInterval !== TEMPERATURE.COLD.INTERVAL) {
debugLog('temperature', 'COLD file: ' + filepath);
fw.temperature = 'cold';
fw.pollInterval = TEMPERATURE.COLD.INTERVAL;
}
} else {
if (fw.pollInterval !== TEMPERATURE.COLDEST.INTERVAL) {
debugLog('temperature', 'COLDEST file: ' + filepath);
fw.temperature = 'coldest';
fw.pollInterval = TEMPERATURE.COLDEST.INTERVAL;
}
}
} // if fw is not awake then cap the polling interval
// this prevents recently modified/created files prior to watching
// from being considered as HOT FILES, i.e., files that are
// actively being modified
if (!fw._awake && !fw.active) {
if (fw.pollInterval < TEMPERATURE.DORMANT.INTERVAL) {
fw.pollInterval = TEMPERATURE.DORMANT.INTERVAL;
fw.temperature = 'dormant';
}
}
if (!api._disableCpuSmoothing && Date.now() - START_TIME > api._CPU_SMOOTHING_DELAY) {
var _stats = api._stats;
var stats = _stats;
if (stats.cpu) {
fw.pollInterval = fw.pollInterval + Math.pow(1 + stats.cpu, 1.75);
}
fw.pollInterval += stats.extraTime;
}
if (fw.active) {
// active files should always be fast
if (fw.pollInterval > TEMPERATURE.WARM.INTERVAL) {
fw.pollInterval = TEMPERATURE.WARM.INTERVAL;
fw.temperature = 'active-warm';
}
} // fw.pollInterval = fw.pollInterval * Math.log( Math.E + ( 100 / fw.pollInterval ) )
}
},{"fs":undefined,"minimatch":undefined,"path":undefined,"redstar":undefined}]},{},[1])(1)
});