Files
Foundry-VTT-Docker/resources/app/node_modules/peggy/bin/watcher.js
2025-01-04 00:34:03 +01:00

123 lines
3.4 KiB
JavaScript

"use strict";
const fs = require("fs");
const path = require("path");
const { EventEmitter } = require("events");
// This may have to be tweaked based on experience.
const DEBOUNCE_MS = 100;
const CLOSING = Symbol("CLOSING");
const ERROR = Symbol("ERROR");
/**
* Relatively feature-free file watcher that deals with some of the
* idiosyncrasies of fs.watch. On some OS's, change notifications are doubled
* up. Watch the owning directory instead of the file, so that when the file
* doesn't exist then gets created we get a change notification instead of an
* error. When the file is moved in or out of the directory, don't track the
* inode of the original file. No notification is given on file deletion,
* just when the file is ready to be read.
*/
class Watcher extends EventEmitter {
/**
* Creates an instance of Watcher. This only works for files in a small
* number of directories.
*
* @param {string[]} filenames The files to watch. Should be one or more
* strings, each of which is the name of a plain file, not a directory,
* pipe, etc.
*/
constructor(...filenames) {
super();
const resolved = new Set(filenames.map(fn => path.resolve(fn)));
const dirs = new Set([...resolved].map(fn => path.dirname(fn)));
this.timeout = null;
this.watchers = [];
for (const dir of dirs) {
// eslint-disable-next-line func-style -- Needs "this"
const changed = (_typ, fn) => {
if (typeof this.timeout === "symbol") {
return;
}
const filename = path.join(dir, fn);
// Might be a different file changing in one of the target dirs
if (resolved.has(filename)) {
if (!this.timeout) {
fs.stat(filename, (er, stats) => {
if (!er && stats.isFile()) {
this.emit("change", filename, stats);
}
});
} else {
clearTimeout(this.timeout);
}
// De-bounce, across all files
this.timeout = setTimeout(() => {
this.timeout = null;
}, Watcher.interval);
}
};
const w = fs.watch(dir);
w.on("error", er => {
const t = this.timeout;
this.timeout = ERROR;
if (t && (typeof t !== "symbol")) {
clearTimeout(t);
}
this.emit("error", er);
this.close();
});
w.on("change", changed);
this.watchers.push(w);
}
// Fire initial time if file exists.
setImmediate(() => {
if (this.watchers.length > 0) {
// First watcher will correspond to the directory of the first filename.
const w = this.watchers[0];
w.emit("change", "initial", path.basename([...resolved][0]));
}
});
}
/**
* Close the watcher. Safe to call multiple times.
*
* @returns {Promise<void>} Always resolves.
*/
close() {
// Stop any more events from firing, immediately
const t = this.timeout;
if (t) {
if (typeof t !== "symbol") {
this.timeout = CLOSING;
clearTimeout(t);
}
}
const p = [];
for (const w of this.watchers) {
p.push(new Promise(resolve => {
w.once("close", resolve);
}));
w.close();
}
return Promise.all(p).then(() => {
this.watchers = [];
if (t !== ERROR) {
this.emit("close");
}
});
}
}
Watcher.interval = DEBOUNCE_MS;
module.exports = Watcher;