nstdlib-nightly
Version:
Node.js standard library converted to runtime-agnostic ES modules.
284 lines (241 loc) • 7.7 kB
JavaScript
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/internal/fs/recursive_watch.js
import * as __hoisted_internal_event_target__ from "nstdlib/lib/internal/event_target";
import { EventEmitter } from "nstdlib/lib/events";
import * as assert from "nstdlib/lib/internal/assert";
import { AbortError, codes as __codes__ } from "nstdlib/lib/internal/errors";
import { getValidatedPath } from "nstdlib/lib/internal/fs/utils";
import { kFSWatchStart, StatWatcher } from "nstdlib/lib/internal/fs/watchers";
import { kEmptyObject } from "nstdlib/lib/internal/util";
import {
validateBoolean,
validateAbortSignal,
} from "nstdlib/lib/internal/validators";
import {
basename as pathBasename,
join as pathJoin,
relative as pathRelative,
resolve as pathResolve,
} from "nstdlib/lib/path";
import * as __hoisted_fs__ from "nstdlib/lib/fs";
const { ERR_INVALID_ARG_VALUE } = __codes__;
let internalSync;
function lazyLoadFsSync() {
internalSync ??= __hoisted_fs__;
return internalSync;
}
let kResistStopPropagation;
class FSWatcher extends EventEmitter {
#options = null;
#closed = false;
#files = new Map();
#watchers = new Map();
#symbolicFiles = new Set();
#rootPath = pathResolve();
#watchingFile = false;
constructor(options = kEmptyObject) {
super();
assert(typeof options === "object");
const { persistent, recursive, signal, encoding } = options;
// TODO(anonrig): Add non-recursive support to non-native-watcher for IBMi & AIX support.
if (recursive != null) {
validateBoolean(recursive, "options.recursive");
}
if (persistent != null) {
validateBoolean(persistent, "options.persistent");
}
if (signal != null) {
validateAbortSignal(signal, "options.signal");
}
if (encoding != null) {
// This is required since on macOS and Windows it throws ERR_INVALID_ARG_VALUE
if (typeof encoding !== "string") {
throw new ERR_INVALID_ARG_VALUE(encoding, "options.encoding");
}
}
this.#options = { persistent, recursive, signal, encoding };
}
close() {
if (this.#closed) {
return;
}
this.#closed = true;
for (const file of this.#files.keys()) {
this.#watchers.get(file)?.close();
this.#watchers.delete(file);
}
this.#files.clear();
this.#symbolicFiles.clear();
this.emit("close");
}
#unwatchFiles(file) {
this.#symbolicFiles.delete(file);
for (const filename of this.#files.keys()) {
if (String.prototype.startsWith.call(filename, file)) {
this.#files.delete(filename);
this.#watchers.get(filename)?.close();
this.#watchers.delete(filename);
}
}
}
#watchFolder(folder) {
const { readdirSync } = lazyLoadFsSync();
try {
const files = readdirSync(folder, {
withFileTypes: true,
});
for (const file of files) {
if (this.#closed) {
break;
}
const f = pathJoin(folder, file.name);
if (!this.#files.has(f)) {
this.emit("change", "rename", pathRelative(this.#rootPath, f));
if (file.isSymbolicLink()) {
this.#symbolicFiles.add(f);
}
try {
this.#watchFile(f);
if (file.isDirectory() && !file.isSymbolicLink()) {
this.#watchFolder(f);
}
} catch (err) {
// Ignore ENOENT
if (err.code !== "ENOENT") {
throw err;
}
}
}
}
} catch (error) {
this.emit("error", error);
}
}
#watchFile(file) {
if (this.#closed) {
return;
}
const { watch, statSync } = lazyLoadFsSync();
if (this.#files.has(file)) {
return;
}
{
const existingStat = statSync(file);
this.#files.set(file, existingStat);
}
const watcher = watch(
file,
{
persistent: this.#options.persistent,
},
(eventType, filename) => {
const existingStat = this.#files.get(file);
let currentStats;
try {
currentStats = statSync(file);
this.#files.set(file, currentStats);
} catch {
// This happens if the file was removed
}
if (
currentStats === undefined ||
(currentStats.birthtimeMs === 0 && existingStat.birthtimeMs !== 0)
) {
// The file is now deleted
this.#files.delete(file);
this.#watchers.delete(file);
watcher.close();
this.emit("change", "rename", pathRelative(this.#rootPath, file));
this.#unwatchFiles(file);
} else if (file === this.#rootPath && this.#watchingFile) {
// This case will only be triggered when watching a file with fs.watch
this.emit("change", "change", pathBasename(file));
} else if (this.#symbolicFiles.has(file)) {
// Stats from watchFile does not return correct value for currentStats.isSymbolicLink()
// Since it is only valid when using fs.lstat(). Therefore, check the existing symbolic files.
this.emit("change", "rename", pathRelative(this.#rootPath, file));
} else if (currentStats.isDirectory()) {
this.#watchFolder(file);
} else {
// Watching a directory will trigger a change event for child files)
this.emit("change", "change", pathRelative(this.#rootPath, file));
}
},
);
this.#watchers.set(file, watcher);
}
[kFSWatchStart](filename) {
filename = pathResolve(getValidatedPath(filename));
try {
const file = lazyLoadFsSync().statSync(filename);
this.#rootPath = filename;
this.#closed = false;
this.#watchingFile = file.isFile();
this.#watchFile(filename);
if (file.isDirectory()) {
this.#watchFolder(filename);
}
} catch (error) {
if (error.code === "ENOENT") {
error.filename = filename;
throw error;
}
}
}
ref() {
this.#files.forEach((file) => {
if (file instanceof StatWatcher) {
file.ref();
}
});
}
unref() {
this.#files.forEach((file) => {
if (file instanceof StatWatcher) {
file.unref();
}
});
}
[SymbolAsyncIterator]() {
const { signal } = this.#options;
const promiseExecutor =
signal == null
? (resolve) => {
this.once("change", (eventType, filename) => {
resolve({ __proto__: null, value: { eventType, filename } });
});
}
: (resolve, reject) => {
const onAbort = () => {
this.close();
reject(new AbortError(undefined, { cause: signal.reason }));
};
if (signal.aborted) return onAbort();
kResistStopPropagation ??=
__hoisted_internal_event_target__.kResistStopPropagation;
signal.addEventListener("abort", onAbort, {
__proto__: null,
once: true,
[kResistStopPropagation]: true,
});
this.once("change", (eventType, filename) => {
signal.removeEventListener("abort", onAbort);
resolve({ __proto__: null, value: { eventType, filename } });
});
};
return {
next: () =>
this.#closed
? { __proto__: null, done: true }
: new Promise(promiseExecutor),
return: () => {
this.close();
return { __proto__: null, done: true };
},
[SymbolAsyncIterator]() {
return this;
},
};
}
}
export { FSWatcher };
export { kFSWatchStart };