249 lines
24 KiB
JavaScript
249 lines
24 KiB
JavaScript
|
/* IMPORT */
|
||
|
import path from 'node:path';
|
||
|
import { DEBOUNCE, DEPTH, LIMIT, HAS_NATIVE_RECURSION, IS_WINDOWS } from './constants.js';
|
||
|
import { FSTargetEvent, FSWatcherEvent, TargetEvent } from './enums.js';
|
||
|
import Utils from './utils.js';
|
||
|
/* MAIN */
|
||
|
class WatcherHandler {
|
||
|
/* CONSTRUCTOR */
|
||
|
constructor(watcher, config, base) {
|
||
|
this.base = base;
|
||
|
this.watcher = watcher;
|
||
|
this.handler = config.handler;
|
||
|
this.fswatcher = config.watcher;
|
||
|
this.options = config.options;
|
||
|
this.folderPath = config.folderPath;
|
||
|
this.filePath = config.filePath;
|
||
|
this.handlerBatched = this.base ? this.base.onWatcherEvent.bind(this.base) : this._makeHandlerBatched(this.options.debounce); //UGLY
|
||
|
}
|
||
|
/* HELPERS */
|
||
|
_isSubRoot(targetPath) {
|
||
|
if (this.filePath) {
|
||
|
return targetPath === this.filePath;
|
||
|
}
|
||
|
else {
|
||
|
return targetPath === this.folderPath || Utils.fs.isSubPath(this.folderPath, targetPath);
|
||
|
}
|
||
|
}
|
||
|
_makeHandlerBatched(delay = DEBOUNCE) {
|
||
|
return (() => {
|
||
|
let lock = this.watcher._readyWait; // ~Ensuring no two flushes are active in parallel, or before the watcher is ready
|
||
|
let initials = [];
|
||
|
let regulars = new Set();
|
||
|
const flush = async (initials, regulars) => {
|
||
|
const initialEvents = this.options.ignoreInitial ? [] : initials;
|
||
|
const regularEvents = await this.eventsPopulate([...regulars]);
|
||
|
const events = this.eventsDeduplicate([...initialEvents, ...regularEvents]);
|
||
|
this.onTargetEvents(events);
|
||
|
};
|
||
|
const flushDebounced = Utils.lang.debounce(() => {
|
||
|
if (this.watcher.isClosed())
|
||
|
return;
|
||
|
lock = flush(initials, regulars);
|
||
|
initials = [];
|
||
|
regulars = new Set();
|
||
|
}, delay);
|
||
|
return async (event, targetPath = '', isInitial = false) => {
|
||
|
if (isInitial) { // Poll immediately
|
||
|
await this.eventsPopulate([targetPath], initials, true);
|
||
|
}
|
||
|
else { // Poll later
|
||
|
regulars.add(targetPath);
|
||
|
}
|
||
|
lock.then(flushDebounced);
|
||
|
};
|
||
|
})();
|
||
|
}
|
||
|
/* EVENT HELPERS */
|
||
|
eventsDeduplicate(events) {
|
||
|
if (events.length < 2)
|
||
|
return events;
|
||
|
const targetsEventPrev = {};
|
||
|
return events.reduce((acc, event) => {
|
||
|
const [targetEvent, targetPath] = event;
|
||
|
const targetEventPrev = targetsEventPrev[targetPath];
|
||
|
if (targetEvent === targetEventPrev)
|
||
|
return acc; // Same event, ignoring
|
||
|
if (targetEvent === TargetEvent.CHANGE && targetEventPrev === TargetEvent.ADD)
|
||
|
return acc; // "change" after "add", ignoring
|
||
|
targetsEventPrev[targetPath] = targetEvent;
|
||
|
acc.push(event);
|
||
|
return acc;
|
||
|
}, []);
|
||
|
}
|
||
|
async eventsPopulate(targetPaths, events = [], isInitial = false) {
|
||
|
await Promise.all(targetPaths.map(async (targetPath) => {
|
||
|
const targetEvents = await this.watcher._poller.update(targetPath, this.options.pollingTimeout);
|
||
|
await Promise.all(targetEvents.map(async (event) => {
|
||
|
events.push([event, targetPath]);
|
||
|
if (event === TargetEvent.ADD_DIR) {
|
||
|
await this.eventsPopulateAddDir(targetPaths, targetPath, events, isInitial);
|
||
|
}
|
||
|
else if (event === TargetEvent.UNLINK_DIR) {
|
||
|
await this.eventsPopulateUnlinkDir(targetPaths, targetPath, events, isInitial);
|
||
|
}
|
||
|
}));
|
||
|
}));
|
||
|
return events;
|
||
|
}
|
||
|
;
|
||
|
async eventsPopulateAddDir(targetPaths, targetPath, events = [], isInitial = false) {
|
||
|
if (isInitial)
|
||
|
return events;
|
||
|
const depth = this.options.recursive ? this.options.depth ?? DEPTH : Math.min(1, this.options.depth ?? DEPTH);
|
||
|
const limit = this.options.limit ?? LIMIT;
|
||
|
const [directories, files] = await Utils.fs.readdir(targetPath, this.options.ignore, depth, limit, this.watcher._closeSignal);
|
||
|
const targetSubPaths = [...directories, ...files];
|
||
|
await Promise.all(targetSubPaths.map(targetSubPath => {
|
||
|
if (this.watcher.isIgnored(targetSubPath, this.options.ignore))
|
||
|
return;
|
||
|
if (targetPaths.includes(targetSubPath))
|
||
|
return;
|
||
|
return this.eventsPopulate([targetSubPath], events, true);
|
||
|
}));
|
||
|
return events;
|
||
|
}
|
||
|
async eventsPopulateUnlinkDir(targetPaths, targetPath, events = [], isInitial = false) {
|
||
|
if (isInitial)
|
||
|
return events;
|
||
|
for (const folderPathOther of this.watcher._poller.stats.keys()) {
|
||
|
if (!Utils.fs.isSubPath(targetPath, folderPathOther))
|
||
|
continue;
|
||
|
if (targetPaths.includes(folderPathOther))
|
||
|
continue;
|
||
|
await this.eventsPopulate([folderPathOther], events, true);
|
||
|
}
|
||
|
return events;
|
||
|
}
|
||
|
/* EVENT HANDLERS */
|
||
|
onTargetAdd(targetPath) {
|
||
|
if (this._isSubRoot(targetPath)) {
|
||
|
if (this.options.renameDetection) {
|
||
|
this.watcher._locker.getLockTargetAdd(targetPath, this.options.renameTimeout);
|
||
|
}
|
||
|
else {
|
||
|
this.watcher.event(TargetEvent.ADD, targetPath);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
onTargetAddDir(targetPath) {
|
||
|
if (targetPath !== this.folderPath && this.options.recursive && (!HAS_NATIVE_RECURSION && this.options.native !== false)) {
|
||
|
this.watcher.watchDirectory(targetPath, this.options, this.handler, undefined, this.base || this);
|
||
|
}
|
||
|
if (this._isSubRoot(targetPath)) {
|
||
|
if (this.options.renameDetection) {
|
||
|
this.watcher._locker.getLockTargetAddDir(targetPath, this.options.renameTimeout);
|
||
|
}
|
||
|
else {
|
||
|
this.watcher.event(TargetEvent.ADD_DIR, targetPath);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
onTargetChange(targetPath) {
|
||
|
if (this._isSubRoot(targetPath)) {
|
||
|
this.watcher.event(TargetEvent.CHANGE, targetPath);
|
||
|
}
|
||
|
}
|
||
|
onTargetUnlink(targetPath) {
|
||
|
this.watcher.watchersClose(path.dirname(targetPath), targetPath, false);
|
||
|
if (this._isSubRoot(targetPath)) {
|
||
|
if (this.options.renameDetection) {
|
||
|
this.watcher._locker.getLockTargetUnlink(targetPath, this.options.renameTimeout);
|
||
|
}
|
||
|
else {
|
||
|
this.watcher.event(TargetEvent.UNLINK, targetPath);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
onTargetUnlinkDir(targetPath) {
|
||
|
this.watcher.watchersClose(path.dirname(targetPath), targetPath, false);
|
||
|
this.watcher.watchersClose(targetPath);
|
||
|
if (this._isSubRoot(targetPath)) {
|
||
|
if (this.options.renameDetection) {
|
||
|
this.watcher._locker.getLockTargetUnlinkDir(targetPath, this.options.renameTimeout);
|
||
|
}
|
||
|
else {
|
||
|
this.watcher.event(TargetEvent.UNLINK_DIR, targetPath);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
onTargetEvent(event) {
|
||
|
const [targetEvent, targetPath] = event;
|
||
|
if (targetEvent === TargetEvent.ADD) {
|
||
|
this.onTargetAdd(targetPath);
|
||
|
}
|
||
|
else if (targetEvent === TargetEvent.ADD_DIR) {
|
||
|
this.onTargetAddDir(targetPath);
|
||
|
}
|
||
|
else if (targetEvent === TargetEvent.CHANGE) {
|
||
|
this.onTargetChange(targetPath);
|
||
|
}
|
||
|
else if (targetEvent === TargetEvent.UNLINK) {
|
||
|
this.onTargetUnlink(targetPath);
|
||
|
}
|
||
|
else if (targetEvent === TargetEvent.UNLINK_DIR) {
|
||
|
this.onTargetUnlinkDir(targetPath);
|
||
|
}
|
||
|
}
|
||
|
onTargetEvents(events) {
|
||
|
for (const event of events) {
|
||
|
this.onTargetEvent(event);
|
||
|
}
|
||
|
}
|
||
|
onWatcherEvent(event, targetPath, isInitial = false) {
|
||
|
return this.handlerBatched(event, targetPath, isInitial);
|
||
|
}
|
||
|
onWatcherChange(event = FSTargetEvent.CHANGE, targetName) {
|
||
|
if (this.watcher.isClosed())
|
||
|
return;
|
||
|
const targetPath = path.resolve(this.folderPath, targetName || '');
|
||
|
if (this.filePath && targetPath !== this.folderPath && targetPath !== this.filePath)
|
||
|
return;
|
||
|
if (this.watcher.isIgnored(targetPath, this.options.ignore))
|
||
|
return;
|
||
|
this.onWatcherEvent(event, targetPath);
|
||
|
}
|
||
|
onWatcherError(error) {
|
||
|
if (IS_WINDOWS && error.code === 'EPERM') { // This may happen when a folder is deleted
|
||
|
this.onWatcherChange(FSTargetEvent.CHANGE, '');
|
||
|
}
|
||
|
else {
|
||
|
this.watcher.error(error);
|
||
|
}
|
||
|
}
|
||
|
/* API */
|
||
|
async init() {
|
||
|
await this.initWatcherEvents();
|
||
|
await this.initInitialEvents();
|
||
|
}
|
||
|
async initWatcherEvents() {
|
||
|
const onChange = this.onWatcherChange.bind(this);
|
||
|
this.fswatcher.on(FSWatcherEvent.CHANGE, onChange);
|
||
|
const onError = this.onWatcherError.bind(this);
|
||
|
this.fswatcher.on(FSWatcherEvent.ERROR, onError);
|
||
|
}
|
||
|
async initInitialEvents() {
|
||
|
const isInitial = !this.watcher.isReady(); // "isInitial" => is ignorable via the "ignoreInitial" option
|
||
|
if (this.filePath) { // Single initial path
|
||
|
if (this.watcher._poller.stats.has(this.filePath))
|
||
|
return; // Already polled
|
||
|
await this.onWatcherEvent(FSTargetEvent.CHANGE, this.filePath, isInitial);
|
||
|
}
|
||
|
else { // Multiple initial paths
|
||
|
const depth = this.options.recursive && (HAS_NATIVE_RECURSION && this.options.native !== false) ? this.options.depth ?? DEPTH : Math.min(1, this.options.depth ?? DEPTH);
|
||
|
const limit = this.options.limit ?? LIMIT;
|
||
|
const [directories, files] = await Utils.fs.readdir(this.folderPath, this.options.ignore, depth, limit, this.watcher._closeSignal, this.options.readdirMap);
|
||
|
const targetPaths = [this.folderPath, ...directories, ...files];
|
||
|
await Promise.all(targetPaths.map(targetPath => {
|
||
|
if (this.watcher._poller.stats.has(targetPath))
|
||
|
return; // Already polled
|
||
|
if (this.watcher.isIgnored(targetPath, this.options.ignore))
|
||
|
return;
|
||
|
return this.onWatcherEvent(FSTargetEvent.CHANGE, targetPath, isInitial);
|
||
|
}));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
/* EXPORT */
|
||
|
export default WatcherHandler;
|
||
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoid2F0Y2hlcl9oYW5kbGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvd2F0Y2hlcl9oYW5kbGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUNBLFlBQVk7QUFFWixPQUFPLElBQUksTUFBTSxXQUFXLENBQUM7QUFDN0IsT0FBTyxFQUFDLFFBQVEsRUFBRSxLQUFLLEVBQUUsS0FBSyxFQUFFLG9CQUFvQixFQUFFLFVBQVUsRUFBQyxNQUFNLGdCQUFnQixDQUFDO0FBQ3hGLE9BQU8sRUFBQyxhQUFhLEVBQUUsY0FBYyxFQUFFLFdBQVcsRUFBQyxNQUFNLFlBQVksQ0FBQztBQUN0RSxPQUFPLEtBQUssTUFBTSxZQUFZLENBQUM7QUFJL0IsVUFBVTtBQUVWLE1BQU0sY0FBYztJQWFsQixpQkFBaUI7SUFFakIsWUFBYyxPQUFnQixFQUFFLE1BQXFCLEVBQUUsSUFBcUI7UUFFMUUsSUFBSSxDQUFDLElBQUksR0FBRyxJQUFJLENBQUM7UUFDakIsSUFBSSxDQUFDLE9BQU8sR0FBRyxPQUFPLENBQUM7UUFDdkIsSUFBSSxDQUFDLE9BQU8sR0FBRyxNQUFNLENBQUMsT0FBTyxDQUFDO1FBQzlCLElBQUksQ0FBQyxTQUFTLEdBQUcsTUFBTSxDQUFDLE9BQU8sQ0FBQztRQUNoQyxJQUFJLENBQUMsT0FBTyxHQUFHLE1BQU0sQ0FBQyxPQUFPLENBQUM7UUFDOUIsSUFBSSxDQUFDLFVBQVUsR0FBRyxNQUFNLENBQUMsVUFBVSxDQUFDO1FBQ3BDLElBQUksQ0FBQyxRQUFRLEdBQUcsTUFBTSxDQUFDLFFBQVEsQ0FBQztRQUVoQyxJQUFJLENBQUMsY0FBYyxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsY0FBYyxDQUFDLElBQUksQ0FBRyxJQUFJLENBQUMsSUFBSSxDQUFFLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxtQkFBbUIsQ0FBRyxJQUFJLENBQUMsT0FBTyxDQUFDLFFBQVEsQ0FBRSxD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
|