json-q
Version:
Retrieves values from JSON objects (and JavaScript objects) by css-selector-like query (includes attribute filters and array flattening).
266 lines (241 loc) • 6.75 kB
JavaScript
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
var fs = require("fs");
var path = require("path");
var async = require("async");
function OldNodeWatchFileSystem(inputFileSystem) {
this.inputFileSystem = inputFileSystem;
}
module.exports = OldNodeWatchFileSystem;
/**
*
* @param files {String[]} a sorted array of paths to files
* @param dirs {String[]} a sorted array of paths to directories
* @param startTime {number} the virtual start time
* @param delay {number} in ms, the time to wait to signal after the first change
* @param callback {function(err, filesModified: String[], dirsModified: String[], fileTimestamps: Object, dirTimestamps: Object)] called once after change plus delay
* @param callbackUndelayed {function()} called once after first change
*/
OldNodeWatchFileSystem.prototype.watch = function(files, dirs, missing, startTime, delay, callback, callbackUndelayed) {
var inputFileSystem = this.inputFileSystem;
if(!callbackUndelayed) callbackUndelayed = function() {};
var closed = false;
var fileTimestamps = {};
var dirTimestamps = {};
var filesModified = {};
var dirsModified = {};
var lastChangeTime;
startTime = Math.floor(startTime / 1000) * 1000; // only 1 second accuracy
var directories = {};
dirs.forEach(function(dir) {
directories[dir] = {
context: dir,
files: []
};
});
files.forEach(function(file) {
var dir = path.dirname(file);
if(!directories[dir]) directories[dir] = {
files: []
};
directories[dir].files.push(file);
});
var items = Object.keys(directories).map(function(dir) {
directories[dir].path = dir;
return directories[dir];
});
items.sort(function(a, b) {
if(a.path === b.path) return 0;
return a.path < b.path ? -1 : 1;
});
items.forEach(function(item) {
if(item.files) {
item.files.sort();
}
});
var initialChange = false;
var change = function() {
initialChange = true;
};
function readStat(item, callback) {
if(item.context) {
fs.readdir(item.path, function(err, files) {
function onTimestamp(ts) {
if(!dirTimestamps[item.context] || dirTimestamps[item.context] < ts)
dirTimestamps[item.context] = ts;
if(ts >= startTime) {
dirsModified[item.context] = true;
change(item.path);
}
return callback();
}
if(err) return onTimestamp(Infinity);
async.map(files, function(file, callback) {
file = path.join(item.path, file);
var isFile = false;
if(item.files) {
if(binarySearch(item.files, function(path) {
if(path === file) return 0;
return path < file ? -1 : 1;
}) >= 0) {
isFile = true;
}
}
fs.stat(file, function(err, stat) {
var ts = err ? Infinity : stat.mtime.getTime();
if(isFile) {
fileTimestamps[file] = ts;
if(ts >= startTime) filesModified[file] = true;
}
return callback(null, ts);
});
}, function(err, timestamps) {
if(err) return onTimestamp(Infinity);
var ts = timestamps.reduce(function(max, ts) {
if(ts > max)
return ts;
return max;
}, 0);
onTimestamp(ts);
});
});
} else {
async.forEach(item.files, function(file, callback) {
fs.stat(file, function(err, stat) {
var ts = err ? Infinity : stat.mtime.getTime();
fileTimestamps[file] = ts;
if(ts >= startTime) {
filesModified[file] = true;
change(file);
}
return callback(null, ts);
});
}, callback);
}
}
async.forEach(items, function processItem(item, callback) {
var isRunning = false;
var isScheduled = false;
item.watcher = fs.watch(item.path, function() {
if(isRunning) {
isScheduled = true;
return;
}
isRunning = true;
readStat(item, done);
});
if(item.context) {
item.children = [];
fs.readdir(item.path, function(err, files) {
if(err) {
change();
onWatcherApplied();
return;
}
async.forEach(files, function(file, callback) {
file = path.join(item.path, file);
fs.stat(file, function(err, stat) {
if(err) {
change(file);
callback();
return;
}
if(!stat.isDirectory()) return callback();
var subitem = {
path: file,
context: item.context
};
item.children.push(subitem);
processItem(subitem, callback);
});
}, onWatcherApplied);
});
} else onWatcherApplied();
function onWatcherApplied() {
readStat(item, function() {
callback();
done();
});
}
function done() {
if(closed) return;
if(isScheduled) {
isScheduled = false;
readStat(item, done);
} else {
isRunning = false;
}
}
}, function() {
var timeout;
if(initialChange) {
callbackUndelayed();
if(delay) {
lastChangeTime = Date.now();
change = restartDelay;
timeout = setTimeout(onTimeout, delay);
} else onTimeout();
} else {
change = function() {
callbackUndelayed();
if(delay) {
lastChangeTime = Date.now();
change = restartDelay;
timeout = setTimeout(onTimeout, delay);
} else {
change = function() {};
onTimeout();
}
};
}
function restartDelay() {
lastChangeTime = Date.now();
clearTimeout(timeout);
timeout = setTimeout(onTimeout, delay);
}
});
// 7.
function onTimeout() {
var nextSecond = Math.ceil(lastChangeTime / 1000) * 1000;
var timeToNextSecond = nextSecond - Date.now();
if(timeToNextSecond > 0) {
setTimeout(onTimeout, timeToNextSecond);
return;
}
change = function() {};
if(closed) return;
var outdatedFiles = Object.keys(filesModified).sort();
var outdatedDirs = Object.keys(dirsModified).sort();
if(inputFileSystem && inputFileSystem.purge) {
inputFileSystem.purge(outdatedFiles);
inputFileSystem.purge(outdatedDirs);
}
callback(null, outdatedFiles, outdatedDirs, [], fileTimestamps, dirTimestamps);
close();
}
function close() {
closed = true;
items.forEach(function closeItem(item) {
item.watcher.close();
if(item.children) item.children.forEach(closeItem);
});
}
return {
close: close,
pause: close
};
};
function binarySearch(array, comparator) {
var left = 0;
var right = array.length - 1;
while(left <= right) {
var middle = ((left + right) / 2) | 0;
var comp = comparator(array[middle]);
if(comp === 0) return middle;
if(comp > 0) right = middle - 1;
if(comp < 0) left = middle + 1;
}
return -1;
}