From 78abae13b7a5ea5f4d0a280c035195c44df5eb5a Mon Sep 17 00:00:00 2001 From: Philipp Kunz Date: Thu, 18 Apr 2024 21:12:37 +0200 Subject: [PATCH] update --- .gitignore | 16 + dist_ts/constants.d.ts | 13 + dist_ts/constants.js | 17 + dist_ts/dettle/debounce.d.ts | 7 + dist_ts/dettle/debounce.js | 92 + dist_ts/dettle/index.d.ts | 3 + dist_ts/dettle/index.js | 6 + dist_ts/dettle/throttle.d.ts | 6 + dist_ts/dettle/throttle.js | 13 + dist_ts/dettle/types.d.ts | 11 + dist_ts/dettle/types.js | 3 + dist_ts/enums.d.ts | 28 + dist_ts/enums.js | 36 + dist_ts/lazy_map_set.d.ts | 10 + dist_ts/lazy_map_set.js | 82 + dist_ts/promise-make-naked/constants.d.ts | 2 + dist_ts/promise-make-naked/constants.js | 5 + dist_ts/promise-make-naked/index.d.ts | 6 + dist_ts/promise-make-naked/index.js | 32 + dist_ts/promise-make-naked/types.d.ts | 11 + dist_ts/promise-make-naked/types.js | 3 + dist_ts/tiny-readdir/index.d.ts | 3 + dist_ts/tiny-readdir/index.js | 179 ++ dist_ts/tiny-readdir/types.d.ts | 28 + dist_ts/tiny-readdir/types.js | 3 + dist_ts/tiny-readdir/utils.d.ts | 8 + dist_ts/tiny-readdir/utils.js | 23 + dist_ts/types.d.ts | 77 + dist_ts/types.js | 3 + dist_ts/utils.d.ts | 41 + dist_ts/utils.js | 121 ++ dist_ts/watcher.d.ts | 55 + dist_ts/watcher.js | 397 ++++ dist_ts/watcher_handler.d.ts | 36 + dist_ts/watcher_handler.js | 249 +++ dist_ts/watcher_locker.d.ts | 32 + dist_ts/watcher_locker.js | 140 ++ dist_ts/watcher_locks_resolver.d.ts | 12 + dist_ts/watcher_locks_resolver.js | 43 + dist_ts/watcher_poller.d.ts | 17 + dist_ts/watcher_poller.js | 117 ++ dist_ts/watcher_stats.d.ts | 17 + dist_ts/watcher_stats.js | 30 + license | 21 + npmextra.json | 5 + package.json | 26 + pnpm-lock.yaml | 921 +++++++++ readme.md | 242 +++ test/hooks.js | 167 ++ test/index.js | 2218 +++++++++++++++++++++ test/tree.js | 112 ++ ts/constants.ts | 32 + ts/dettle/debounce.ts | 152 ++ ts/dettle/index.ts | 9 + ts/dettle/license | 21 + ts/dettle/throttle.ts | 21 + ts/dettle/types.ts | 14 + ts/enums.ts | 38 + ts/lazy_map_set.ts | 144 ++ ts/promise-make-naked/constants.ts | 8 + ts/promise-make-naked/index.ts | 53 + ts/promise-make-naked/license | 21 + ts/promise-make-naked/types.ts | 19 + ts/tiny-readdir/index.ts | 246 +++ ts/tiny-readdir/license | 21 + ts/tiny-readdir/types.ts | 38 + ts/tiny-readdir/utils.ts | 43 + ts/types.ts | 104 + ts/utils.ts | 208 ++ ts/watcher.ts | 655 ++++++ ts/watcher_handler.ts | 427 ++++ ts/watcher_locker.ts | 202 ++ ts/watcher_locks_resolver.ts | 73 + ts/watcher_poller.ts | 193 ++ ts/watcher_stats.ts | 64 + tsconfig.json | 14 + 76 files changed, 8565 insertions(+) create mode 100644 .gitignore create mode 100644 dist_ts/constants.d.ts create mode 100644 dist_ts/constants.js create mode 100644 dist_ts/dettle/debounce.d.ts create mode 100644 dist_ts/dettle/debounce.js create mode 100644 dist_ts/dettle/index.d.ts create mode 100644 dist_ts/dettle/index.js create mode 100644 dist_ts/dettle/throttle.d.ts create mode 100644 dist_ts/dettle/throttle.js create mode 100644 dist_ts/dettle/types.d.ts create mode 100644 dist_ts/dettle/types.js create mode 100644 dist_ts/enums.d.ts create mode 100644 dist_ts/enums.js create mode 100644 dist_ts/lazy_map_set.d.ts create mode 100644 dist_ts/lazy_map_set.js create mode 100644 dist_ts/promise-make-naked/constants.d.ts create mode 100644 dist_ts/promise-make-naked/constants.js create mode 100644 dist_ts/promise-make-naked/index.d.ts create mode 100644 dist_ts/promise-make-naked/index.js create mode 100644 dist_ts/promise-make-naked/types.d.ts create mode 100644 dist_ts/promise-make-naked/types.js create mode 100644 dist_ts/tiny-readdir/index.d.ts create mode 100644 dist_ts/tiny-readdir/index.js create mode 100644 dist_ts/tiny-readdir/types.d.ts create mode 100644 dist_ts/tiny-readdir/types.js create mode 100644 dist_ts/tiny-readdir/utils.d.ts create mode 100644 dist_ts/tiny-readdir/utils.js create mode 100644 dist_ts/types.d.ts create mode 100644 dist_ts/types.js create mode 100644 dist_ts/utils.d.ts create mode 100644 dist_ts/utils.js create mode 100644 dist_ts/watcher.d.ts create mode 100644 dist_ts/watcher.js create mode 100644 dist_ts/watcher_handler.d.ts create mode 100644 dist_ts/watcher_handler.js create mode 100644 dist_ts/watcher_locker.d.ts create mode 100644 dist_ts/watcher_locker.js create mode 100644 dist_ts/watcher_locks_resolver.d.ts create mode 100644 dist_ts/watcher_locks_resolver.js create mode 100644 dist_ts/watcher_poller.d.ts create mode 100644 dist_ts/watcher_poller.js create mode 100644 dist_ts/watcher_stats.d.ts create mode 100644 dist_ts/watcher_stats.js create mode 100644 license create mode 100644 npmextra.json create mode 100755 package.json create mode 100644 pnpm-lock.yaml create mode 100644 readme.md create mode 100644 test/hooks.js create mode 100644 test/index.js create mode 100644 test/tree.js create mode 100644 ts/constants.ts create mode 100644 ts/dettle/debounce.ts create mode 100755 ts/dettle/index.ts create mode 100644 ts/dettle/license create mode 100644 ts/dettle/throttle.ts create mode 100644 ts/dettle/types.ts create mode 100644 ts/enums.ts create mode 100644 ts/lazy_map_set.ts create mode 100644 ts/promise-make-naked/constants.ts create mode 100755 ts/promise-make-naked/index.ts create mode 100644 ts/promise-make-naked/license create mode 100644 ts/promise-make-naked/types.ts create mode 100755 ts/tiny-readdir/index.ts create mode 100644 ts/tiny-readdir/license create mode 100644 ts/tiny-readdir/types.ts create mode 100644 ts/tiny-readdir/utils.ts create mode 100644 ts/types.ts create mode 100644 ts/utils.ts create mode 100644 ts/watcher.ts create mode 100644 ts/watcher_handler.ts create mode 100644 ts/watcher_locker.ts create mode 100644 ts/watcher_locks_resolver.ts create mode 100644 ts/watcher_poller.ts create mode 100644 ts/watcher_stats.ts create mode 100755 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3256379 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +*~ +*.err +*.log +._* +.cache +.fseventsd +.DocumentRevisions* +.DS_Store +.TemporaryItems +.Trashes +Thumbs.db + +dist +node_modules +package-lock.json +__TREES__ diff --git a/dist_ts/constants.d.ts b/dist_ts/constants.d.ts new file mode 100644 index 0000000..459bc12 --- /dev/null +++ b/dist_ts/constants.d.ts @@ -0,0 +1,13 @@ +/// +declare const DEBOUNCE = 300; +declare const DEPTH = 20; +declare const LIMIT = 10000000; +declare const PLATFORM: NodeJS.Platform; +declare const IS_LINUX: boolean; +declare const IS_MAC: boolean; +declare const IS_WINDOWS: boolean; +declare const HAS_NATIVE_RECURSION: boolean; +declare const POLLING_INTERVAL = 3000; +declare const POLLING_TIMEOUT = 20000; +declare const RENAME_TIMEOUT = 1250; +export { DEBOUNCE, DEPTH, LIMIT, HAS_NATIVE_RECURSION, IS_LINUX, IS_MAC, IS_WINDOWS, PLATFORM, POLLING_INTERVAL, POLLING_TIMEOUT, RENAME_TIMEOUT }; diff --git a/dist_ts/constants.js b/dist_ts/constants.js new file mode 100644 index 0000000..0528124 --- /dev/null +++ b/dist_ts/constants.js @@ -0,0 +1,17 @@ +/* IMPORT */ +import os from 'node:os'; +/* MAIN */ +const DEBOUNCE = 300; +const DEPTH = 20; +const LIMIT = 10_000_000; +const PLATFORM = os.platform(); +const IS_LINUX = (PLATFORM === 'linux'); +const IS_MAC = (PLATFORM === 'darwin'); +const IS_WINDOWS = (PLATFORM === 'win32'); +const HAS_NATIVE_RECURSION = IS_MAC || IS_WINDOWS; +const POLLING_INTERVAL = 3000; +const POLLING_TIMEOUT = 20000; +const RENAME_TIMEOUT = 1250; +/* EXPORT */ +export { DEBOUNCE, DEPTH, LIMIT, HAS_NATIVE_RECURSION, IS_LINUX, IS_MAC, IS_WINDOWS, PLATFORM, POLLING_INTERVAL, POLLING_TIMEOUT, RENAME_TIMEOUT }; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uc3RhbnRzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvY29uc3RhbnRzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUNBLFlBQVk7QUFFWixPQUFPLEVBQUUsTUFBTSxTQUFTLENBQUM7QUFFekIsVUFBVTtBQUVWLE1BQU0sUUFBUSxHQUFHLEdBQUcsQ0FBQztBQUVyQixNQUFNLEtBQUssR0FBRyxFQUFFLENBQUM7QUFFakIsTUFBTSxLQUFLLEdBQUcsVUFBVSxDQUFDO0FBRXpCLE1BQU0sUUFBUSxHQUFHLEVBQUUsQ0FBQyxRQUFRLEVBQUcsQ0FBQztBQUVoQyxNQUFNLFFBQVEsR0FBRyxDQUFFLFFBQVEsS0FBSyxPQUFPLENBQUUsQ0FBQztBQUUxQyxNQUFNLE1BQU0sR0FBRyxDQUFFLFFBQVEsS0FBSyxRQUFRLENBQUUsQ0FBQztBQUV6QyxNQUFNLFVBQVUsR0FBRyxDQUFFLFFBQVEsS0FBSyxPQUFPLENBQUUsQ0FBQztBQUU1QyxNQUFNLG9CQUFvQixHQUFHLE1BQU0sSUFBSSxVQUFVLENBQUM7QUFFbEQsTUFBTSxnQkFBZ0IsR0FBRyxJQUFJLENBQUM7QUFFOUIsTUFBTSxlQUFlLEdBQUcsS0FBSyxDQUFDO0FBRTlCLE1BQU0sY0FBYyxHQUFHLElBQUksQ0FBQztBQUU1QixZQUFZO0FBRVosT0FBTyxFQUFDLFFBQVEsRUFBRSxLQUFLLEVBQUUsS0FBSyxFQUFFLG9CQUFvQixFQUFFLFFBQVEsRUFBRSxNQUFNLEVBQUUsVUFBVSxFQUFFLFFBQVEsRUFBRSxnQkFBZ0IsRUFBRSxlQUFlLEVBQUUsY0FBYyxFQUFDLENBQUMifQ== \ No newline at end of file diff --git a/dist_ts/dettle/debounce.d.ts b/dist_ts/dettle/debounce.d.ts new file mode 100644 index 0000000..089ab9e --- /dev/null +++ b/dist_ts/dettle/debounce.d.ts @@ -0,0 +1,7 @@ +import type { FN, Debounced } from './types.js'; +declare const debounce: (fn: FN, wait?: number, options?: { + leading?: boolean; + trailing?: boolean; + maxWait?: number; +}) => Debounced; +export default debounce; diff --git a/dist_ts/dettle/debounce.js b/dist_ts/dettle/debounce.js new file mode 100644 index 0000000..ac4dfc5 --- /dev/null +++ b/dist_ts/dettle/debounce.js @@ -0,0 +1,92 @@ +/* IMPORT */ +/* MAIN */ +const debounce = (fn, wait = 1, options) => { + /* VARIABLES */ + wait = Math.max(1, wait); + const leading = options?.leading ?? false; + const trailing = options?.trailing ?? true; + const maxWait = Math.max(options?.maxWait ?? Infinity, wait); + let args; + let timeout; + let timestampCall = 0; + let timestampInvoke = 0; + /* HELPERS */ + const getInstantData = () => { + const timestamp = Date.now(); + const elapsedCall = timestamp - timestampCall; + const elapsedInvoke = timestamp - timestampInvoke; + const isInvoke = (elapsedCall >= wait || elapsedInvoke >= maxWait); + return [timestamp, isInvoke]; + }; + const invoke = (timestamp) => { + timestampInvoke = timestamp; + if (!args) + return; // This should never happen + const _args = args; + args = undefined; + fn.apply(undefined, _args); + }; + const onCancel = () => { + resetTimeout(0); + }; + const onFlush = () => { + if (!timeout) + return; + onCancel(); + invoke(Date.now()); + }; + const onLeading = (timestamp) => { + timestampInvoke = timestamp; + if (leading) + return invoke(timestamp); + }; + const onTrailing = (timestamp) => { + if (trailing && args) + return invoke(timestamp); + args = undefined; + }; + const onTimeout = () => { + timeout = undefined; + const [timestamp, isInvoking] = getInstantData(); + if (isInvoking) + return onTrailing(timestamp); + return updateTimeout(timestamp); + }; + const updateTimeout = (timestamp) => { + const elapsedCall = timestamp - timestampCall; + const elapsedInvoke = timestamp - timestampInvoke; + const remainingCall = wait - elapsedCall; + const remainingInvoke = maxWait - elapsedInvoke; + const ms = Math.min(remainingCall, remainingInvoke); + return resetTimeout(ms); + }; + const resetTimeout = (ms) => { + if (timeout) + clearTimeout(timeout); + if (ms <= 0) + return; + timeout = setTimeout(onTimeout, ms); + }; + /* DEBOUNCED */ + const debounced = (...argsLatest) => { + const [timestamp, isInvoking] = getInstantData(); + const hadTimeout = !!timeout; + args = argsLatest; + timestampCall = timestamp; + if (isInvoking || !timeout) + resetTimeout(wait); + if (isInvoking) { + if (!hadTimeout) + return onLeading(timestamp); + return invoke(timestamp); + } + }; + /* DEBOUNCED UTILITIES */ + debounced.cancel = onCancel; + debounced.flush = onFlush; + /* RETURN */ + return debounced; +}; +/* EXPORT */ +export default debounce; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGVib3VuY2UuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9kZXR0bGUvZGVib3VuY2UudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQ0EsWUFBWTtBQUlaLFVBQVU7QUFFVixNQUFNLFFBQVEsR0FBRyxDQUEyQixFQUFxQixFQUFFLE9BQWUsQ0FBQyxFQUFFLE9BQXFFLEVBQW9CLEVBQUU7SUFFOUssZUFBZTtJQUVmLElBQUksR0FBRyxJQUFJLENBQUMsR0FBRyxDQUFHLENBQUMsRUFBRSxJQUFJLENBQUUsQ0FBQztJQUU1QixNQUFNLE9BQU8sR0FBRyxPQUFPLEVBQUUsT0FBTyxJQUFJLEtBQUssQ0FBQztJQUMxQyxNQUFNLFFBQVEsR0FBRyxPQUFPLEVBQUUsUUFBUSxJQUFJLElBQUksQ0FBQztJQUMzQyxNQUFNLE9BQU8sR0FBRyxJQUFJLENBQUMsR0FBRyxDQUFHLE9BQU8sRUFBRSxPQUFPLElBQUksUUFBUSxFQUFFLElBQUksQ0FBRSxDQUFDO0lBRWhFLElBQUksSUFBc0IsQ0FBQztJQUMzQixJQUFJLE9BQWtELENBQUM7SUFDdkQsSUFBSSxhQUFhLEdBQUcsQ0FBQyxDQUFDO0lBQ3RCLElBQUksZUFBZSxHQUFHLENBQUMsQ0FBQztJQUV4QixhQUFhO0lBRWIsTUFBTSxjQUFjLEdBQUcsR0FBc0IsRUFBRTtRQUU3QyxNQUFNLFNBQVMsR0FBRyxJQUFJLENBQUMsR0FBRyxFQUFHLENBQUM7UUFDOUIsTUFBTSxXQUFXLEdBQUcsU0FBUyxHQUFHLGFBQWEsQ0FBQztRQUM5QyxNQUFNLGFBQWEsR0FBRyxTQUFTLEdBQUcsZUFBZSxDQUFDO1FBQ2xELE1BQU0sUUFBUSxHQUFHLENBQUUsV0FBVyxJQUFJLElBQUksSUFBSSxhQUFhLElBQUksT0FBTyxDQUFFLENBQUM7UUFFckUsT0FBTyxDQUFDLFNBQVMsRUFBRSxRQUFRLENBQUMsQ0FBQztJQUUvQixDQUFDLENBQUM7SUFFRixNQUFNLE1BQU0sR0FBRyxDQUFFLFNBQWlCLEVB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 \ No newline at end of file diff --git a/dist_ts/dettle/index.d.ts b/dist_ts/dettle/index.d.ts new file mode 100644 index 0000000..73e5340 --- /dev/null +++ b/dist_ts/dettle/index.d.ts @@ -0,0 +1,3 @@ +import debounce from './debounce.js'; +import throttle from './throttle.js'; +export { debounce, throttle }; diff --git a/dist_ts/dettle/index.js b/dist_ts/dettle/index.js new file mode 100644 index 0000000..f4f271e --- /dev/null +++ b/dist_ts/dettle/index.js @@ -0,0 +1,6 @@ +/* IMPORT */ +import debounce from './debounce.js'; +import throttle from './throttle.js'; +/* EXPORT */ +export { debounce, throttle }; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9kZXR0bGUvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQ0EsWUFBWTtBQUVaLE9BQU8sUUFBUSxNQUFNLGVBQWUsQ0FBQztBQUNyQyxPQUFPLFFBQVEsTUFBTSxlQUFlLENBQUM7QUFFckMsWUFBWTtBQUVaLE9BQU8sRUFBQyxRQUFRLEVBQUUsUUFBUSxFQUFDLENBQUMifQ== \ No newline at end of file diff --git a/dist_ts/dettle/throttle.d.ts b/dist_ts/dettle/throttle.d.ts new file mode 100644 index 0000000..2dc7e74 --- /dev/null +++ b/dist_ts/dettle/throttle.d.ts @@ -0,0 +1,6 @@ +import type { FN, Throttled } from './types.js'; +declare const throttle: (fn: FN, wait?: number, options?: { + leading?: boolean; + trailing?: boolean; +}) => Throttled; +export default throttle; diff --git a/dist_ts/dettle/throttle.js b/dist_ts/dettle/throttle.js new file mode 100644 index 0000000..5331cd9 --- /dev/null +++ b/dist_ts/dettle/throttle.js @@ -0,0 +1,13 @@ +/* IMPORT */ +import debounce from './debounce.js'; +/* MAIN */ +const throttle = (fn, wait = 1, options) => { + return debounce(fn, wait, { + maxWait: wait, + leading: options?.leading ?? true, + trailing: options?.trailing ?? true + }); +}; +/* EXPORT */ +export default throttle; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGhyb3R0bGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9kZXR0bGUvdGhyb3R0bGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQ0EsWUFBWTtBQUVaLE9BQU8sUUFBUSxNQUFNLGVBQWUsQ0FBQztBQUdyQyxVQUFVO0FBRVYsTUFBTSxRQUFRLEdBQUcsQ0FBMkIsRUFBcUIsRUFBRSxPQUFlLENBQUMsRUFBRSxPQUFtRCxFQUFvQixFQUFFO0lBRTVKLE9BQU8sUUFBUSxDQUFHLEVBQUUsRUFBRSxJQUFJLEVBQUU7UUFDMUIsT0FBTyxFQUFFLElBQUk7UUFDYixPQUFPLEVBQUUsT0FBTyxFQUFFLE9BQU8sSUFBSSxJQUFJO1FBQ2pDLFFBQVEsRUFBRSxPQUFPLEVBQUUsUUFBUSxJQUFJLElBQUk7S0FDcEMsQ0FBQyxDQUFDO0FBRUwsQ0FBQyxDQUFDO0FBRUYsWUFBWTtBQUVaLGVBQWUsUUFBUSxDQUFDIn0= \ No newline at end of file diff --git a/dist_ts/dettle/types.d.ts b/dist_ts/dettle/types.d.ts new file mode 100644 index 0000000..94fd30b --- /dev/null +++ b/dist_ts/dettle/types.d.ts @@ -0,0 +1,11 @@ +type Callback = () => void; +type FN = (...args: Args) => Return; +type Debounced = FN & { + cancel: Callback; + flush: Callback; +}; +type Throttled = FN & { + cancel: Callback; + flush: Callback; +}; +export type { Callback, FN, Debounced, Throttled }; diff --git a/dist_ts/dettle/types.js b/dist_ts/dettle/types.js new file mode 100644 index 0000000..fc46415 --- /dev/null +++ b/dist_ts/dettle/types.js @@ -0,0 +1,3 @@ +/* MAIN */ +export {}; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHlwZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9kZXR0bGUvdHlwZXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQ0EsVUFBVSJ9 \ No newline at end of file diff --git a/dist_ts/enums.d.ts b/dist_ts/enums.d.ts new file mode 100644 index 0000000..cc6b18f --- /dev/null +++ b/dist_ts/enums.d.ts @@ -0,0 +1,28 @@ +declare const enum FileType { + DIR = 1, + FILE = 2 +} +declare const enum FSTargetEvent { + CHANGE = "change", + RENAME = "rename" +} +declare const enum FSWatcherEvent { + CHANGE = "change", + ERROR = "error" +} +declare const enum TargetEvent { + ADD = "add", + ADD_DIR = "addDir", + CHANGE = "change", + RENAME = "rename", + RENAME_DIR = "renameDir", + UNLINK = "unlink", + UNLINK_DIR = "unlinkDir" +} +declare const enum WatcherEvent { + ALL = "all", + CLOSE = "close", + ERROR = "error", + READY = "ready" +} +export { FileType, FSTargetEvent, FSWatcherEvent, TargetEvent, WatcherEvent }; diff --git a/dist_ts/enums.js b/dist_ts/enums.js new file mode 100644 index 0000000..37f95ff --- /dev/null +++ b/dist_ts/enums.js @@ -0,0 +1,36 @@ +/* MAIN */ +var FileType; +(function (FileType) { + FileType[FileType["DIR"] = 1] = "DIR"; + FileType[FileType["FILE"] = 2] = "FILE"; +})(FileType || (FileType = {})); +var FSTargetEvent; +(function (FSTargetEvent) { + FSTargetEvent["CHANGE"] = "change"; + FSTargetEvent["RENAME"] = "rename"; +})(FSTargetEvent || (FSTargetEvent = {})); +var FSWatcherEvent; +(function (FSWatcherEvent) { + FSWatcherEvent["CHANGE"] = "change"; + FSWatcherEvent["ERROR"] = "error"; +})(FSWatcherEvent || (FSWatcherEvent = {})); +var TargetEvent; +(function (TargetEvent) { + TargetEvent["ADD"] = "add"; + TargetEvent["ADD_DIR"] = "addDir"; + TargetEvent["CHANGE"] = "change"; + TargetEvent["RENAME"] = "rename"; + TargetEvent["RENAME_DIR"] = "renameDir"; + TargetEvent["UNLINK"] = "unlink"; + TargetEvent["UNLINK_DIR"] = "unlinkDir"; +})(TargetEvent || (TargetEvent = {})); +var WatcherEvent; +(function (WatcherEvent) { + WatcherEvent["ALL"] = "all"; + WatcherEvent["CLOSE"] = "close"; + WatcherEvent["ERROR"] = "error"; + WatcherEvent["READY"] = "ready"; +})(WatcherEvent || (WatcherEvent = {})); +/* EXPORT */ +export { FileType, FSTargetEvent, FSWatcherEvent, TargetEvent, WatcherEvent }; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZW51bXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9lbnVtcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFDQSxVQUFVO0FBRVYsSUFBVyxRQUdWO0FBSEQsV0FBVyxRQUFRO0lBQ2pCLHFDQUFPLENBQUE7SUFDUCx1Q0FBUSxDQUFBO0FBQ1YsQ0FBQyxFQUhVLFFBQVEsS0FBUixRQUFRLFFBR2xCO0FBRUQsSUFBVyxhQUdWO0FBSEQsV0FBVyxhQUFhO0lBQ3RCLGtDQUFpQixDQUFBO0lBQ2pCLGtDQUFpQixDQUFBO0FBQ25CLENBQUMsRUFIVSxhQUFhLEtBQWIsYUFBYSxRQUd2QjtBQUVELElBQVcsY0FHVjtBQUhELFdBQVcsY0FBYztJQUN2QixtQ0FBaUIsQ0FBQTtJQUNqQixpQ0FBZSxDQUFBO0FBQ2pCLENBQUMsRUFIVSxjQUFjLEtBQWQsY0FBYyxRQUd4QjtBQUVELElBQVcsV0FRVjtBQVJELFdBQVcsV0FBVztJQUNwQiwwQkFBVyxDQUFBO0lBQ1gsaUNBQWtCLENBQUE7SUFDbEIsZ0NBQWlCLENBQUE7SUFDakIsZ0NBQWlCLENBQUE7SUFDakIsdUNBQXdCLENBQUE7SUFDeEIsZ0NBQWlCLENBQUE7SUFDakIsdUNBQXdCLENBQUE7QUFDMUIsQ0FBQyxFQVJVLFdBQVcsS0FBWCxXQUFXLFFBUXJCO0FBRUQsSUFBVyxZQUtWO0FBTEQsV0FBVyxZQUFZO0lBQ3JCLDJCQUFXLENBQUE7SUFDWCwrQkFBZSxDQUFBO0lBQ2YsK0JBQWUsQ0FBQTtJQUNmLCtCQUFlLENBQUE7QUFDakIsQ0FBQyxFQUxVLFlBQVksS0FBWixZQUFZLFFBS3RCO0FBRUQsWUFBWTtBQUVaLE9BQU8sRUFBQyxRQUFRLEVBQUUsYUFBYSxFQUFFLGNBQWMsRUFBRSxXQUFXLEVBQUUsWUFBWSxFQUFDLENBQUMifQ== \ No newline at end of file diff --git a/dist_ts/lazy_map_set.d.ts b/dist_ts/lazy_map_set.d.ts new file mode 100644 index 0000000..69b9c8d --- /dev/null +++ b/dist_ts/lazy_map_set.d.ts @@ -0,0 +1,10 @@ +declare class LazyMapSet { + private map; + clear(): void; + delete(key: K, value?: V): boolean; + find(key: K, iterator: (value: V) => boolean): V | undefined; + get(key: K): Set | V | undefined; + has(key: K, value?: V): boolean; + set(key: K, value: V): this; +} +export default LazyMapSet; diff --git a/dist_ts/lazy_map_set.js b/dist_ts/lazy_map_set.js new file mode 100644 index 0000000..9bac4c8 --- /dev/null +++ b/dist_ts/lazy_map_set.js @@ -0,0 +1,82 @@ +/* IMPORT */ +import Utils from './utils.js'; +/* MAIN */ +//TODO: Maybe publish this as a standalone module +class LazyMapSet { + constructor() { + /* VARIABLES */ + this.map = new Map(); + } + /* API */ + clear() { + this.map.clear(); + } + delete(key, value) { + if (Utils.lang.isUndefined(value)) { + return this.map.delete(key); + } + else if (this.map.has(key)) { + const values = this.map.get(key); + if (Utils.lang.isSet(values)) { + const deleted = values.delete(value); + if (!values.size) { + this.map.delete(key); + } + return deleted; + } + else if (values === value) { + this.map.delete(key); + return true; + } + } + return false; + } + find(key, iterator) { + if (this.map.has(key)) { + const values = this.map.get(key); + if (Utils.lang.isSet(values)) { + return Array.from(values).find(iterator); + } + else if (iterator(values)) { //TSC + return values; + } + } + return undefined; + } + get(key) { + return this.map.get(key); + } + has(key, value) { + if (Utils.lang.isUndefined(value)) { + return this.map.has(key); + } + else if (this.map.has(key)) { + const values = this.map.get(key); + if (Utils.lang.isSet(values)) { + return values.has(value); + } + else { + return (values === value); + } + } + return false; + } + set(key, value) { + if (this.map.has(key)) { + const values = this.map.get(key); + if (Utils.lang.isSet(values)) { + values.add(value); + } + else if (values !== value) { + this.map.set(key, new Set([values, value])); //TSC + } + } + else { + this.map.set(key, value); + } + return this; + } +} +/* EXPORT */ +export default LazyMapSet; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibGF6eV9tYXBfc2V0LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvbGF6eV9tYXBfc2V0LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUNBLFlBQVk7QUFFWixPQUFPLEtBQUssTUFBTSxZQUFZLENBQUM7QUFFL0IsVUFBVTtBQUVWLGlEQUFpRDtBQUVqRCxNQUFNLFVBQVU7SUFBaEI7UUFFRSxlQUFlO1FBRVAsUUFBRyxHQUF1QixJQUFJLEdBQUcsRUFBRyxDQUFDO0lBOEgvQyxDQUFDO0lBNUhDLFNBQVM7SUFFVCxLQUFLO1FBRUgsSUFBSSxDQUFDLEdBQUcsQ0FBQyxLQUFLLEVBQUcsQ0FBQztJQUVwQixDQUFDO0lBRUQsTUFBTSxDQUFHLEdBQU0sRUFBRSxLQUFTO1FBRXhCLElBQUssS0FBSyxDQUFDLElBQUksQ0FBQyxXQUFXLENBQUcsS0FBSyxDQUFFLEVBQUcsQ0FBQztZQUV2QyxPQUFPLElBQUksQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFHLEdBQUcsQ0FBRSxDQUFDO1FBRWpDLENBQUM7YUFBTSxJQUFLLElBQUksQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFHLEdBQUcsQ0FBRSxFQUFHLENBQUM7WUFFbEMsTUFBTSxNQUFNLEdBQUcsSUFBSSxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUcsR0FBRyxDQUFFLENBQUM7WUFFcEMsSUFBSyxLQUFLLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBRyxNQUFNLENBQUUsRUFBRyxDQUFDO2dCQUVsQyxNQUFNLE9BQU8sR0FBRyxNQUFNLENBQUMsTUFBTSxDQUFHLEtBQUssQ0FBRSxDQUFDO2dCQUV4QyxJQUFLLENBQUMsTUFBTSxDQUFDLElBQUksRUFBRyxDQUFDO29CQUVuQixJQUFJLENBQUMsR0FBRyxDQUFDLE1BQU0sQ0FBRyxHQUFHLENBQUUsQ0FBQztnQkFFMUIsQ0FBQztnQkFFRCxPQUFPLE9BQU8sQ0FBQztZQUVqQixDQUFDO2lCQUFNLElBQUssTUFBTSxLQUFLLEtBQUssRUFBRyxD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 \ No newline at end of file diff --git a/dist_ts/promise-make-naked/constants.d.ts b/dist_ts/promise-make-naked/constants.d.ts new file mode 100644 index 0000000..36fa87f --- /dev/null +++ b/dist_ts/promise-make-naked/constants.d.ts @@ -0,0 +1,2 @@ +declare const NOOP: () => void; +export { NOOP }; diff --git a/dist_ts/promise-make-naked/constants.js b/dist_ts/promise-make-naked/constants.js new file mode 100644 index 0000000..8e17f30 --- /dev/null +++ b/dist_ts/promise-make-naked/constants.js @@ -0,0 +1,5 @@ +/* MAIN */ +const NOOP = () => { }; +/* EXPORT */ +export { NOOP }; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uc3RhbnRzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHMvcHJvbWlzZS1tYWtlLW5ha2VkL2NvbnN0YW50cy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFDQSxVQUFVO0FBRVYsTUFBTSxJQUFJLEdBQUcsR0FBUyxFQUFFLEdBQUUsQ0FBQyxDQUFDO0FBRTVCLFlBQVk7QUFFWixPQUFPLEVBQUMsSUFBSSxFQUFDLENBQUMifQ== \ No newline at end of file diff --git a/dist_ts/promise-make-naked/index.d.ts b/dist_ts/promise-make-naked/index.d.ts new file mode 100644 index 0000000..05b41f1 --- /dev/null +++ b/dist_ts/promise-make-naked/index.d.ts @@ -0,0 +1,6 @@ +import type { Result } from './types.js'; +declare const makeNakedPromise: { + (): Result; + wrap(fn: (result: Result) => void): Promise; +}; +export default makeNakedPromise; diff --git a/dist_ts/promise-make-naked/index.js b/dist_ts/promise-make-naked/index.js new file mode 100644 index 0000000..b795529 --- /dev/null +++ b/dist_ts/promise-make-naked/index.js @@ -0,0 +1,32 @@ +/* IMPORT */ +import { NOOP } from './constants.js'; +/* MAIN */ +const makeNakedPromise = () => { + let resolve = NOOP; + let reject = NOOP; + let resolved = false; + let rejected = false; + const promise = new Promise((res, rej) => { + resolve = value => { + resolved = true; + return res(value); + }; + reject = value => { + rejected = true; + return rej(value); + }; + }); + const isPending = () => !resolved && !rejected; + const isResolved = () => resolved; + const isRejected = () => rejected; + return { promise, resolve, reject, isPending, isResolved, isRejected }; +}; +/* UTILITIES */ +makeNakedPromise.wrap = async (fn) => { + const result = makeNakedPromise(); + await fn(result); + return result.promise; +}; +/* EXPORT */ +export default makeNakedPromise; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9wcm9taXNlLW1ha2UtbmFrZWQvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQ0EsWUFBWTtBQUVaLE9BQU8sRUFBQyxJQUFJLEVBQUMsTUFBTSxnQkFBZ0IsQ0FBQztBQUdwQyxVQUFVO0FBRVYsTUFBTSxnQkFBZ0IsR0FBRyxHQUFrQixFQUFFO0lBRTNDLElBQUksT0FBTyxHQUFzQixJQUFJLENBQUM7SUFDdEMsSUFBSSxNQUFNLEdBQWtCLElBQUksQ0FBQztJQUVqQyxJQUFJLFFBQVEsR0FBRyxLQUFLLENBQUM7SUFDckIsSUFBSSxRQUFRLEdBQUcsS0FBSyxDQUFDO0lBRXJCLE1BQU0sT0FBTyxHQUFHLElBQUksT0FBTyxDQUFNLENBQUUsR0FBRyxFQUFFLEdBQUcsRUFBUyxFQUFFO1FBRXBELE9BQU8sR0FBRyxLQUFLLENBQUMsRUFBRTtZQUNoQixRQUFRLEdBQUcsSUFBSSxDQUFDO1lBQ2hCLE9BQU8sR0FBRyxDQUFHLEtBQUssQ0FBRSxDQUFDO1FBQ3ZCLENBQUMsQ0FBQztRQUVGLE1BQU0sR0FBRyxLQUFLLENBQUMsRUFBRTtZQUNmLFFBQVEsR0FBRyxJQUFJLENBQUM7WUFDaEIsT0FBTyxHQUFHLENBQUcsS0FBSyxDQUFFLENBQUM7UUFDdkIsQ0FBQyxDQUFDO0lBRUosQ0FBQyxDQUFDLENBQUM7SUFFSCxNQUFNLFNBQVMsR0FBRyxHQUFZLEVBQUUsQ0FBQyxDQUFDLFFBQVEsSUFBSSxDQUFDLFFBQVEsQ0FBQztJQUN4RCxNQUFNLFVBQVUsR0FBRyxHQUFZLEVBQUUsQ0FBQyxRQUFRLENBQUM7SUFDM0MsTUFBTSxVQUFVLEdBQUcsR0FBWSxFQUFFLENBQUMsUUFBUSxDQUFDO0lBRTNDLE9BQU8sRUFBQyxPQUFPLEVBQUUsT0FBTyxFQUFFLE1BQU0sRUFBRSxTQUFTLEVBQUUsVUFBVSxFQUFFLFVBQVUsRUFBQyxDQUFDO0FBRXZFLENBQUMsQ0FBQztBQUVGLGVBQWU7QUFFZixnQkFBZ0IsQ0FBQyxJQUFJLEdBQUcsS0FBSyxFQUFPLEVBQWlDLEVBQWUsRUFBRTtJQUVwRixNQUFNLE1BQU0sR0FBRyxnQkFBZ0IsRUFBTSxDQUFDO0lBRXRDLE1BQU0sRUFBRSxDQUFHLE1BQU0sQ0FBRSxDQUFDO0lBRXBCLE9BQU8sTUFBTSxDQUFDLE9BQU8sQ0FBQztBQUV4QixDQUFDLENBQUM7QUFFRixZQUFZO0FBRVosZUFBZSxnQkFBZ0IsQ0FBQyJ9 \ No newline at end of file diff --git a/dist_ts/promise-make-naked/types.d.ts b/dist_ts/promise-make-naked/types.d.ts new file mode 100644 index 0000000..8626091 --- /dev/null +++ b/dist_ts/promise-make-naked/types.d.ts @@ -0,0 +1,11 @@ +type PromiseResolve = (value: T | PromiseLike) => void; +type PromiseReject = (reason?: unknown) => void; +type Result = { + promise: Promise; + resolve: PromiseResolve; + reject: PromiseReject; + isPending: () => boolean; + isResolved: () => boolean; + isRejected: () => boolean; +}; +export type { PromiseResolve, PromiseReject, Result }; diff --git a/dist_ts/promise-make-naked/types.js b/dist_ts/promise-make-naked/types.js new file mode 100644 index 0000000..f69906e --- /dev/null +++ b/dist_ts/promise-make-naked/types.js @@ -0,0 +1,3 @@ +/* MAIN */ +export {}; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHlwZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9wcm9taXNlLW1ha2UtbmFrZWQvdHlwZXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQ0EsVUFBVSJ9 \ No newline at end of file diff --git a/dist_ts/tiny-readdir/index.d.ts b/dist_ts/tiny-readdir/index.d.ts new file mode 100644 index 0000000..7adbccc --- /dev/null +++ b/dist_ts/tiny-readdir/index.d.ts @@ -0,0 +1,3 @@ +import type { Options, Result } from './types.js'; +declare const readdir: (rootPath: string, options?: Options) => Promise; +export default readdir; diff --git a/dist_ts/tiny-readdir/index.js b/dist_ts/tiny-readdir/index.js new file mode 100644 index 0000000..834368f --- /dev/null +++ b/dist_ts/tiny-readdir/index.js @@ -0,0 +1,179 @@ +/* IMPORT */ +import fs from 'node:fs'; +import path from 'node:path'; +import { isFunction, makeCounterPromise } from './utils.js'; +/* MAIN */ +//TODO: Streamline the type of dirnmaps +const readdir = (rootPath, options) => { + const followSymlinks = options?.followSymlinks ?? false; + const maxDepth = options?.depth ?? Infinity; + const maxPaths = options?.limit ?? Infinity; + const ignore = options?.ignore ?? (() => false); + const isIgnored = isFunction(ignore) ? ignore : (targetPath) => ignore.test(targetPath); + const signal = options?.signal ?? { aborted: false }; + const directories = []; + const directoriesNames = new Set(); + const directoriesNamesToPaths = {}; + const files = []; + const filesNames = new Set(); + const filesNamesToPaths = {}; + const symlinks = []; + const symlinksNames = new Set(); + const symlinksNamesToPaths = {}; + const map = {}; + const visited = new Set(); + const resultEmpty = { directories: [], directoriesNames: new Set(), directoriesNamesToPaths: {}, files: [], filesNames: new Set(), filesNamesToPaths: {}, symlinks: [], symlinksNames: new Set(), symlinksNamesToPaths: {}, map: {} }; + const result = { directories, directoriesNames, directoriesNamesToPaths, files, filesNames, filesNamesToPaths, symlinks, symlinksNames, symlinksNamesToPaths, map }; + const { promise, increment, decrement } = makeCounterPromise(); + let foundPaths = 0; + const handleDirectory = (dirmap, subPath, name, depth) => { + if (visited.has(subPath)) + return; + if (foundPaths >= maxPaths) + return; + foundPaths += 1; + dirmap.directories.push(subPath); + dirmap.directoriesNames.add(name); + // dirmap.directoriesNamesToPaths.propertyIsEnumerable(name) || ( dirmap.directoriesNamesToPaths[name] = [] ); + // dirmap.directoriesNamesToPaths[name].push ( subPath ); + directories.push(subPath); + directoriesNames.add(name); + directoriesNamesToPaths.propertyIsEnumerable(name) || (directoriesNamesToPaths[name] = []); + directoriesNamesToPaths[name].push(subPath); + visited.add(subPath); + if (depth >= maxDepth) + return; + if (foundPaths >= maxPaths) + return; + populateResultFromPath(subPath, depth + 1); + }; + const handleFile = (dirmap, subPath, name) => { + if (visited.has(subPath)) + return; + if (foundPaths >= maxPaths) + return; + foundPaths += 1; + dirmap.files.push(subPath); + dirmap.filesNames.add(name); + // dirmap.filesNamesToPaths.propertyIsEnumerable(name) || ( dirmap.filesNamesToPaths[name] = [] ); + // dirmap.filesNamesToPaths[name].push ( subPath ); + files.push(subPath); + filesNames.add(name); + filesNamesToPaths.propertyIsEnumerable(name) || (filesNamesToPaths[name] = []); + filesNamesToPaths[name].push(subPath); + visited.add(subPath); + }; + const handleSymlink = (dirmap, subPath, name, depth) => { + if (visited.has(subPath)) + return; + if (foundPaths >= maxPaths) + return; + foundPaths += 1; + dirmap.symlinks.push(subPath); + dirmap.symlinksNames.add(name); + // dirmap.symlinksNamesToPaths.propertyIsEnumerable(name) || ( dirmap.symlinksNamesToPaths[name] = [] ); + // dirmap.symlinksNamesToPaths[name].push ( subPath ); + symlinks.push(subPath); + symlinksNames.add(name); + symlinksNamesToPaths.propertyIsEnumerable(name) || (symlinksNamesToPaths[name] = []); + symlinksNamesToPaths[name].push(subPath); + visited.add(subPath); + if (!followSymlinks) + return; + if (depth >= maxDepth) + return; + if (foundPaths >= maxPaths) + return; + populateResultFromSymlink(subPath, depth + 1); + }; + const handleStat = (dirmap, rootPath, name, stat, depth) => { + if (signal.aborted) + return; + if (isIgnored(rootPath)) + return; + if (stat.isDirectory()) { + handleDirectory(dirmap, rootPath, name, depth); + } + else if (stat.isFile()) { + handleFile(dirmap, rootPath, name); + } + else if (stat.isSymbolicLink()) { + handleSymlink(dirmap, rootPath, name, depth); + } + }; + const handleDirent = (dirmap, rootPath, dirent, depth) => { + if (signal.aborted) + return; + const separator = (rootPath === path.sep) ? '' : path.sep; + const name = dirent.name; + const subPath = `${rootPath}${separator}${name}`; + if (isIgnored(subPath)) + return; + if (dirent.isDirectory()) { + handleDirectory(dirmap, subPath, name, depth); + } + else if (dirent.isFile()) { + handleFile(dirmap, subPath, name); + } + else if (dirent.isSymbolicLink()) { + handleSymlink(dirmap, subPath, name, depth); + } + }; + const handleDirents = (dirmap, rootPath, dirents, depth) => { + for (let i = 0, l = dirents.length; i < l; i++) { + handleDirent(dirmap, rootPath, dirents[i], depth); + } + }; + const populateResultFromPath = (rootPath, depth) => { + if (signal.aborted) + return; + if (depth > maxDepth) + return; + if (foundPaths >= maxPaths) + return; + increment(); + fs.readdir(rootPath, { withFileTypes: true }, (error, dirents) => { + if (error) + return decrement(); + if (signal.aborted) + return decrement(); + if (!dirents.length) + return decrement(); + const dirmap = map[rootPath] = { directories: [], directoriesNames: new Set(), directoriesNamesToPaths: {}, files: [], filesNames: new Set(), filesNamesToPaths: {}, symlinks: [], symlinksNames: new Set(), symlinksNamesToPaths: {} }; + handleDirents(dirmap, rootPath, dirents, depth); + decrement(); + }); + }; + const populateResultFromSymlink = async (rootPath, depth) => { + increment(); + fs.realpath(rootPath, (error, realPath) => { + if (error) + return decrement(); + if (signal.aborted) + return decrement(); + fs.stat(realPath, async (error, stat) => { + if (error) + return decrement(); + if (signal.aborted) + return decrement(); + const name = path.basename(realPath); + const dirmap = map[rootPath] = { directories: [], directoriesNames: new Set(), directoriesNamesToPaths: {}, files: [], filesNames: new Set(), filesNamesToPaths: {}, symlinks: [], symlinksNames: new Set(), symlinksNamesToPaths: {} }; + handleStat(dirmap, realPath, name, stat, depth); + decrement(); + }); + }); + }; + const getResult = async (rootPath, depth = 1) => { + rootPath = path.normalize(rootPath); + visited.add(rootPath); + populateResultFromPath(rootPath, depth); + await promise; + if (signal.aborted) + return resultEmpty; + return result; + }; + return getResult(rootPath); +}; +/* EXPORT */ +export default readdir; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy90aW55LXJlYWRkaXIvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQ0EsWUFBWTtBQUVaLE9BQU8sRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUN6QixPQUFPLElBQUksTUFBTSxXQUFXLENBQUM7QUFDN0IsT0FBTyxFQUFDLFVBQVUsRUFBRSxrQkFBa0IsRUFBQyxNQUFNLFlBQVksQ0FBQztBQUcxRCxVQUFVO0FBRVYsdUNBQXVDO0FBRXZDLE1BQU0sT0FBTyxHQUFHLENBQUUsUUFBZ0IsRUFBRSxPQUFpQixFQUFvQixFQUFFO0lBRXpFLE1BQU0sY0FBYyxHQUFHLE9BQU8sRUFBRSxjQUFjLElBQUksS0FBSyxDQUFDO0lBQ3hELE1BQU0sUUFBUSxHQUFHLE9BQU8sRUFBRSxLQUFLLElBQUksUUFBUSxDQUFDO0lBQzVDLE1BQU0sUUFBUSxHQUFHLE9BQU8sRUFBRSxLQUFLLElBQUksUUFBUSxDQUFDO0lBQzVDLE1BQU0sTUFBTSxHQUFHLE9BQU8sRUFBRSxNQUFNLElBQUksQ0FBQyxHQUFHLEVBQUUsQ0FBQyxLQUFLLENBQUMsQ0FBQztJQUNoRCxNQUFNLFNBQVMsR0FBRyxVQUFVLENBQUcsTUFBTSxDQUFFLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsQ0FBRSxVQUFrQixFQUFHLEVBQUUsQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFHLFVBQVUsQ0FBRSxDQUFDO0lBQ3hHLE1BQU0sTUFBTSxHQUFHLE9BQU8sRUFBRSxNQUFNLElBQUksRUFBRSxPQUFPLEVBQUUsS0FBSyxFQUFFLENBQUM7SUFDckQsTUFBTSxXQUFXLEdBQWEsRUFBRSxDQUFDO0lBQ2pDLE1BQU0sZ0JBQWdCLEdBQWdCLElBQUksR0FBRyxFQUFHLENBQUM7SUFDakQsTUFBTSx1QkFBdUIsR0FBNkIsRUFBRSxDQUFDO0lBQzdELE1BQU0sS0FBSyxHQUFhLEVBQUUsQ0FBQztJQUMzQixN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 \ No newline at end of file diff --git a/dist_ts/tiny-readdir/types.d.ts b/dist_ts/tiny-readdir/types.d.ts new file mode 100644 index 0000000..8edef6d --- /dev/null +++ b/dist_ts/tiny-readdir/types.d.ts @@ -0,0 +1,28 @@ +type Callback = () => void; +type Options = { + depth?: number; + limit?: number; + followSymlinks?: boolean; + ignore?: ((targetPath: string) => boolean) | RegExp; + signal?: { + aborted: boolean; + }; +}; +type ResultDirectory = { + directories: string[]; + directoriesNames: Set; + directoriesNamesToPaths: Record; + files: string[]; + filesNames: Set; + filesNamesToPaths: Record; + symlinks: string[]; + symlinksNames: Set; + symlinksNamesToPaths: Record; +}; +type ResultDirectories = { + [path: string]: ResultDirectory; +}; +type Result = ResultDirectory & { + map: ResultDirectories; +}; +export type { Callback, Options, ResultDirectory, ResultDirectories, Result }; diff --git a/dist_ts/tiny-readdir/types.js b/dist_ts/tiny-readdir/types.js new file mode 100644 index 0000000..1265421 --- /dev/null +++ b/dist_ts/tiny-readdir/types.js @@ -0,0 +1,3 @@ +/* HELPERS */ +export {}; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHlwZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy90aW55LXJlYWRkaXIvdHlwZXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQ0EsYUFBYSJ9 \ No newline at end of file diff --git a/dist_ts/tiny-readdir/utils.d.ts b/dist_ts/tiny-readdir/utils.d.ts new file mode 100644 index 0000000..21b958c --- /dev/null +++ b/dist_ts/tiny-readdir/utils.d.ts @@ -0,0 +1,8 @@ +import type { Callback } from './types.js'; +declare const isFunction: (value: unknown) => value is Function; +declare const makeCounterPromise: () => { + promise: Promise; + increment: Callback; + decrement: Callback; +}; +export { isFunction, makeCounterPromise }; diff --git a/dist_ts/tiny-readdir/utils.js b/dist_ts/tiny-readdir/utils.js new file mode 100644 index 0000000..5d6b3ab --- /dev/null +++ b/dist_ts/tiny-readdir/utils.js @@ -0,0 +1,23 @@ +/* IMPORT */ +import makeNakedPromise from '../promise-make-naked/index.js'; +/* MAIN */ +const isFunction = (value) => { + return (typeof value === 'function'); +}; +const makeCounterPromise = () => { + const { promise, resolve } = makeNakedPromise(); + let counter = 0; + const increment = () => { + counter += 1; + }; + const decrement = () => { + counter -= 1; + if (counter) + return; + resolve(); + }; + return { promise, increment, decrement }; +}; +/* EXPORT */ +export { isFunction, makeCounterPromise }; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXRpbHMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy90aW55LXJlYWRkaXIvdXRpbHMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQ0EsWUFBWTtBQUVaLE9BQU8sZ0JBQWdCLE1BQU0sZ0NBQWdDLENBQUM7QUFHOUQsVUFBVTtBQUVWLE1BQU0sVUFBVSxHQUFHLENBQUUsS0FBYyxFQUFzQixFQUFFO0lBRXpELE9BQU8sQ0FBRSxPQUFPLEtBQUssS0FBSyxVQUFVLENBQUUsQ0FBQztBQUV6QyxDQUFDLENBQUM7QUFFRixNQUFNLGtCQUFrQixHQUFHLEdBQXlFLEVBQUU7SUFFcEcsTUFBTSxFQUFDLE9BQU8sRUFBRSxPQUFPLEVBQUMsR0FBRyxnQkFBZ0IsRUFBUyxDQUFDO0lBRXJELElBQUksT0FBTyxHQUFHLENBQUMsQ0FBQztJQUVoQixNQUFNLFNBQVMsR0FBRyxHQUFTLEVBQUU7UUFFM0IsT0FBTyxJQUFJLENBQUMsQ0FBQztJQUVmLENBQUMsQ0FBQztJQUVGLE1BQU0sU0FBUyxHQUFHLEdBQVMsRUFBRTtRQUUzQixPQUFPLElBQUksQ0FBQyxDQUFDO1FBRWIsSUFBSyxPQUFPO1lBQUcsT0FBTztRQUV0QixPQUFPLEVBQUcsQ0FBQztJQUViLENBQUMsQ0FBQztJQUVGLE9BQU8sRUFBRSxPQUFPLEVBQUUsU0FBUyxFQUFFLFNBQVMsRUFBRSxDQUFDO0FBRTNDLENBQUMsQ0FBQztBQUVGLFlBQVk7QUFFWixPQUFPLEVBQUMsVUFBVSxFQUFFLGtCQUFrQixFQUFDLENBQUMifQ== \ No newline at end of file diff --git a/dist_ts/types.d.ts b/dist_ts/types.d.ts new file mode 100644 index 0000000..5440d32 --- /dev/null +++ b/dist_ts/types.d.ts @@ -0,0 +1,77 @@ +/// +import type { FSWatcher, BigIntStats } from 'node:fs'; +import type { FSTargetEvent, TargetEvent } from './enums.js'; +import type WatcherStats from './watcher_stats.js'; +type ResultDirectory = { + directories: string[]; + directoriesNames: Set; + directoriesNamesToPaths: Record; + files: string[]; + filesNames: Set; + filesNamesToPaths: Record; + symlinks: string[]; + symlinksNames: Set; + symlinksNamesToPaths: Record; +}; +type ResultDirectories = { + [path: string]: ResultDirectory; +}; +type Callback = () => void; +type Disposer = () => void; +type Event = [TargetEvent, Path, Path?]; +type FSHandler = (event?: FSTargetEvent, targetName?: string) => void; +type Handler = (event: TargetEvent, targetPath: Path, targetPathNext?: Path) => void; +type HandlerBatched = (event?: FSTargetEvent, targetPath?: Path, isInitial?: boolean) => Promise; +type Ignore = ((targetPath: Path) => boolean) | RegExp; +type INO = bigint | number; +type Path = string; +type ReaddirMap = ResultDirectories; +type Stats = BigIntStats; +type LocksAdd = Map void>; +type LocksUnlink = Map Path>; +type LocksPair = { + add: LocksAdd; + unlink: LocksUnlink; +}; +type LockConfig = { + ino?: INO; + targetPath: Path; + locks: LocksPair; + events: { + add: TargetEvent.ADD | TargetEvent.ADD_DIR; + change?: TargetEvent.CHANGE; + rename: TargetEvent.RENAME | TargetEvent.RENAME_DIR; + unlink: TargetEvent.UNLINK | TargetEvent.UNLINK_DIR; + }; +}; +type PollerConfig = { + options: WatcherOptions; + targetPath: Path; +}; +type SubwatcherConfig = { + options: WatcherOptions; + targetPath: Path; +}; +type WatcherConfig = { + handler: Handler; + watcher: FSWatcher; + options: WatcherOptions; + folderPath: Path; + filePath?: Path; +}; +type WatcherOptions = { + debounce?: number; + depth?: number; + limit?: number; + ignore?: Ignore; + ignoreInitial?: boolean; + native?: boolean; + persistent?: boolean; + pollingInterval?: number; + pollingTimeout?: number; + readdirMap?: ReaddirMap; + recursive?: boolean; + renameDetection?: boolean; + renameTimeout?: number; +}; +export type { Callback, Disposer, Event, FSHandler, FSWatcher, Handler, HandlerBatched, Ignore, INO, Path, ReaddirMap, Stats, LocksAdd, LocksUnlink, LocksPair, LockConfig, PollerConfig, SubwatcherConfig, WatcherConfig, WatcherOptions, WatcherStats }; diff --git a/dist_ts/types.js b/dist_ts/types.js new file mode 100644 index 0000000..6e27dd9 --- /dev/null +++ b/dist_ts/types.js @@ -0,0 +1,3 @@ +/* IMPORT */ +export {}; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHlwZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy90eXBlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFDQSxZQUFZIn0= \ No newline at end of file diff --git a/dist_ts/utils.d.ts b/dist_ts/utils.d.ts new file mode 100644 index 0000000..a6d35b2 --- /dev/null +++ b/dist_ts/utils.d.ts @@ -0,0 +1,41 @@ +/// +/// +/// +/// +/// +import type { Callback, Ignore, ReaddirMap, Stats } from './types.js'; +declare const Utils: { + lang: { + debounce: (fn: import("./dettle/types.js").FN, wait?: number, options?: { + leading?: boolean; + trailing?: boolean; + maxWait?: number; + }) => import("./dettle/types.js").Debounced; + attempt: (fn: () => T) => Error | T; + castArray: (x: T_1 | T_1[]) => T_1[]; + castError: (exception: unknown) => Error; + defer: (callback: Callback) => NodeJS.Timeout; + isArray: (value: unknown) => value is unknown[]; + isError: (value: unknown) => value is Error; + isFunction: (value: unknown) => value is Function; + isNaN: (value: unknown) => value is number; + isNumber: (value: unknown) => value is number; + isPrimitive: (value: unknown) => value is string | number | bigint | boolean | symbol; + isShallowEqual: (x: any, y: any) => boolean; + isSet: (value: unknown) => value is Set; + isString: (value: unknown) => value is string; + isUndefined: (value: unknown) => value is undefined; + noop: () => undefined; + uniq: (arr: T_2[]) => T_2[]; + }; + fs: { + getDepth: (targetPath: string) => number; + getRealPath: (targetPath: string, native?: boolean) => string | undefined; + isSubPath: (targetPath: string, subPath: string) => boolean; + poll: (targetPath: string, timeout?: number) => Promise; + readdir: (rootPath: string, ignore?: Ignore, depth?: number, limit?: number, signal?: { + aborted: boolean; + }, readdirMap?: ReaddirMap) => Promise<[string[], string[]]>; + }; +}; +export default Utils; diff --git a/dist_ts/utils.js b/dist_ts/utils.js new file mode 100644 index 0000000..0ab0e14 --- /dev/null +++ b/dist_ts/utils.js @@ -0,0 +1,121 @@ +/* IMPORT */ +import { debounce } from './dettle/index.js'; +import fs from 'node:fs'; +import path from 'node:path'; +import sfs from 'stubborn-fs'; +import readdir from './tiny-readdir/index.js'; +import { POLLING_TIMEOUT } from './constants.js'; +/* MAIN */ +const Utils = { + /* LANG API */ + lang: { + debounce, + attempt: (fn) => { + try { + return fn(); + } + catch (error) { + return Utils.lang.castError(error); + } + }, + castArray: (x) => { + return Utils.lang.isArray(x) ? x : [x]; + }, + castError: (exception) => { + if (Utils.lang.isError(exception)) + return exception; + if (Utils.lang.isString(exception)) + return new Error(exception); + return new Error('Unknown error'); + }, + defer: (callback) => { + return setTimeout(callback, 0); + }, + isArray: (value) => { + return Array.isArray(value); + }, + isError: (value) => { + return value instanceof Error; + }, + isFunction: (value) => { + return typeof value === 'function'; + }, + isNaN: (value) => { + return Number.isNaN(value); + }, + isNumber: (value) => { + return typeof value === 'number'; + }, + isPrimitive: (value) => { + if (value === null) + return true; + const type = typeof value; + return type !== 'object' && type !== 'function'; + }, + isShallowEqual: (x, y) => { + if (x === y) + return true; + if (Utils.lang.isNaN(x)) + return Utils.lang.isNaN(y); + if (Utils.lang.isPrimitive(x) || Utils.lang.isPrimitive(y)) + return x === y; + for (const i in x) + if (!(i in y)) + return false; + for (const i in y) + if (x[i] !== y[i]) + return false; + return true; + }, + isSet: (value) => { + return value instanceof Set; + }, + isString: (value) => { + return typeof value === 'string'; + }, + isUndefined: (value) => { + return value === undefined; + }, + noop: () => { + return; + }, + uniq: (arr) => { + if (arr.length < 2) + return arr; + return Array.from(new Set(arr)); + } + }, + /* FS API */ + fs: { + getDepth: (targetPath) => { + return Math.max(0, targetPath.split(path.sep).length - 1); + }, + getRealPath: (targetPath, native) => { + try { + return native ? fs.realpathSync.native(targetPath) : fs.realpathSync(targetPath); + } + catch { + return; + } + }, + isSubPath: (targetPath, subPath) => { + return (subPath.startsWith(targetPath) && subPath[targetPath.length] === path.sep && (subPath.length - targetPath.length) > path.sep.length); + }, + poll: (targetPath, timeout = POLLING_TIMEOUT) => { + return sfs.retry.stat(timeout)(targetPath, { bigint: true }).catch(Utils.lang.noop); + }, + readdir: async (rootPath, ignore, depth = Infinity, limit = Infinity, signal, readdirMap) => { + if (readdirMap && depth === 1 && rootPath in readdirMap) { // Reusing cached data + const result = readdirMap[rootPath]; + return [result.directories, result.files]; + } + else { // Retrieving fresh data + const result = await readdir(rootPath, { depth, limit, ignore, signal }); + return [result.directories, result.files]; + } + } + } +}; +/* EXPORT */ +export default Utils; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXRpbHMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy91dGlscy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFDQSxZQUFZO0FBRVosT0FBTyxFQUFFLFFBQVEsRUFBRSxNQUFNLG1CQUFtQixDQUFDO0FBQzdDLE9BQU8sRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUN6QixPQUFPLElBQUksTUFBTSxXQUFXLENBQUM7QUFDN0IsT0FBTyxHQUFHLE1BQU0sYUFBYSxDQUFDO0FBQzlCLE9BQU8sT0FBTyxNQUFNLHlCQUF5QixDQUFDO0FBQzlDLE9BQU8sRUFBQyxlQUFlLEVBQUMsTUFBTSxnQkFBZ0IsQ0FBQztBQUcvQyxVQUFVO0FBRVYsTUFBTSxLQUFLLEdBQUc7SUFFWixjQUFjO0lBRWQsSUFBSSxFQUFFO1FBRUosUUFBUTtRQUVSLE9BQU8sRUFBRSxDQUFNLEVBQVcsRUFBYyxFQUFFO1lBRXhDLElBQUksQ0FBQztnQkFFSCxPQUFPLEVBQUUsRUFBRyxDQUFDO1lBRWYsQ0FBQztZQUFDLE9BQVEsS0FBYyxFQUFHLENBQUM7Z0JBRTFCLE9BQU8sS0FBSyxDQUFDLElBQUksQ0FBQyxTQUFTLENBQUcsS0FBSyxDQUFFLENBQUM7WUFFeEMsQ0FBQztRQUVILENBQUM7UUFFRCxTQUFTLEVBQUUsQ0FBTSxDQUFVLEVBQVEsRUFBRTtZQUVuQyxPQUFPLEtBQUssQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFHLENBQUMsQ0FBRSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7UUFFNUMsQ0FBQztRQUVELFNBQVMsRUFBRSxDQUFFLFNBQWtCLEVBQVUsRUFBRTtZQUV6QyxJQUFLLEtBQUssQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFHLFNBQVMsQ0FBRTtnQkFBRyxPQUFPLFNBQVMsQ0FBQztZQUV6RCxJQUFLLEtBQUssQ0FBQyxJQUFJLENBQUMsUUFBUSxDQUFHLFNBQVMsQ0FBRTtnQkFBRyxPQUFPLElB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 \ No newline at end of file diff --git a/dist_ts/watcher.d.ts b/dist_ts/watcher.d.ts new file mode 100644 index 0000000..8ba128a --- /dev/null +++ b/dist_ts/watcher.d.ts @@ -0,0 +1,55 @@ +/// +/// +import { EventEmitter } from 'node:events'; +import { TargetEvent } from './enums.js'; +import WatcherHandler from './watcher_handler.js'; +import WatcherLocker from './watcher_locker.js'; +import WatcherPoller from './watcher_poller.js'; +import type { Callback, Disposer, Handler, Ignore, Path, PollerConfig, SubwatcherConfig, WatcherOptions, WatcherConfig } from './types.js'; +declare class Watcher extends EventEmitter { + _closed: boolean; + _ready: boolean; + _closeAborter: AbortController; + _closeSignal: { + aborted: boolean; + }; + _closeWait: Promise; + _readyWait: Promise; + _locker: WatcherLocker; + _roots: Set; + _poller: WatcherPoller; + _pollers: Set; + _subwatchers: Set; + _watchers: Record; + _watchersLock: Promise; + _watchersRestorable: Record; + _watchersRestoreTimeout?: NodeJS.Timeout; + constructor(target?: Path[] | Path | Handler, options?: WatcherOptions | Handler, handler?: Handler); + isClosed(): boolean; + isIgnored(targetPath: Path, ignore?: Ignore): boolean; + isReady(): boolean; + close(): boolean; + error(exception: unknown): boolean; + event(event: TargetEvent, targetPath: Path, targetPathNext?: Path): boolean; + ready(): boolean; + pollerExists(targetPath: Path, options: WatcherOptions): boolean; + subwatcherExists(targetPath: Path, options: WatcherOptions): boolean; + watchersClose(folderPath?: Path, filePath?: Path, recursive?: boolean): void; + watchersLock(callback: Callback): Promise; + watchersRestore(): void; + watcherAdd(config: WatcherConfig, baseWatcherHandler?: WatcherHandler): Promise; + watcherClose(config: WatcherConfig): void; + watcherExists(folderPath: Path, options: WatcherOptions, handler: Handler, filePath?: Path): boolean; + watchDirectories(foldersPaths: Path[], options: WatcherOptions, handler: Handler, filePath?: Path, baseWatcherHandler?: WatcherHandler): Promise; + watchDirectory(folderPath: Path, options: WatcherOptions, handler: Handler, filePath?: Path, baseWatcherHandler?: WatcherHandler): Promise; + watchFileOnce(filePath: Path, options: WatcherOptions, callback: Callback): Promise; + watchFile(filePath: Path, options: WatcherOptions, handler: Handler): Promise; + watchPollingOnce(targetPath: Path, options: WatcherOptions, callback: Callback): Promise; + watchPolling(targetPath: Path, options: WatcherOptions, callback: Callback): Promise; + watchUnknownChild(targetPath: Path, options: WatcherOptions, handler: Handler): Promise; + watchUnknownTarget(targetPath: Path, options: WatcherOptions, handler: Handler): Promise; + watchPaths(targetPaths: Path[], options: WatcherOptions, handler: Handler): Promise; + watchPath(targetPath: Path, options: WatcherOptions, handler: Handler): Promise; + watch(target?: Path[] | Path | Handler, options?: WatcherOptions | Handler, handler?: Handler): Promise; +} +export default Watcher; diff --git a/dist_ts/watcher.js b/dist_ts/watcher.js new file mode 100644 index 0000000..0facf09 --- /dev/null +++ b/dist_ts/watcher.js @@ -0,0 +1,397 @@ +/* IMPORT */ +import { EventEmitter } from 'node:events'; +import fs from 'node:fs'; +import path from 'node:path'; +import { DEPTH, LIMIT, HAS_NATIVE_RECURSION, POLLING_INTERVAL } from './constants.js'; +import { TargetEvent, WatcherEvent } from './enums.js'; +import Utils from './utils.js'; +import WatcherHandler from './watcher_handler.js'; +import WatcherLocker from './watcher_locker.js'; +import WatcherPoller from './watcher_poller.js'; +/* MAIN */ +class Watcher extends EventEmitter { + /* CONSTRUCTOR */ + constructor(target, options, handler) { + super(); + this._closed = false; + this._ready = false; + this._closeAborter = new AbortController(); + this._closeSignal = this._closeAborter.signal; + this.on(WatcherEvent.CLOSE, () => this._closeAborter.abort()); + this._closeWait = new Promise(resolve => this.on(WatcherEvent.CLOSE, resolve)); + this._readyWait = new Promise(resolve => this.on(WatcherEvent.READY, resolve)); + this._locker = new WatcherLocker(this); + this._roots = new Set(); + this._poller = new WatcherPoller(); + this._pollers = new Set(); + this._subwatchers = new Set(); + this._watchers = {}; + this._watchersLock = Promise.resolve(); + this._watchersRestorable = {}; + this.watch(target, options, handler); + } + /* API */ + isClosed() { + return this._closed; + } + isIgnored(targetPath, ignore) { + return !!ignore && (Utils.lang.isFunction(ignore) ? !!ignore(targetPath) : ignore.test(targetPath)); + } + isReady() { + return this._ready; + } + close() { + this._locker.reset(); + this._poller.reset(); + this._roots.clear(); + this.watchersClose(); + if (this.isClosed()) + return false; + this._closed = true; + return this.emit(WatcherEvent.CLOSE); + } + error(exception) { + if (this.isClosed()) + return false; + const error = Utils.lang.castError(exception); + return this.emit(WatcherEvent.ERROR, error); + } + event(event, targetPath, targetPathNext) { + if (this.isClosed()) + return false; + this.emit(WatcherEvent.ALL, event, targetPath, targetPathNext); + return this.emit(event, targetPath, targetPathNext); + } + ready() { + if (this.isClosed() || this.isReady()) + return false; + this._ready = true; + return this.emit(WatcherEvent.READY); + } + pollerExists(targetPath, options) { + for (const poller of this._pollers) { + if (poller.targetPath !== targetPath) + continue; + if (!Utils.lang.isShallowEqual(poller.options, options)) + continue; + return true; + } + return false; + } + subwatcherExists(targetPath, options) { + for (const subwatcher of this._subwatchers) { + if (subwatcher.targetPath !== targetPath) + continue; + if (!Utils.lang.isShallowEqual(subwatcher.options, options)) + continue; + return true; + } + return false; + } + watchersClose(folderPath, filePath, recursive = true) { + if (!folderPath) { + for (const folderPath in this._watchers) { + this.watchersClose(folderPath, filePath, false); + } + } + else { + const configs = this._watchers[folderPath]; + if (configs) { + for (const config of [...configs]) { // It's important to clone the array, as items will be deleted from it also + if (filePath && config.filePath !== filePath) + continue; + this.watcherClose(config); + } + } + if (recursive) { + for (const folderPathOther in this._watchers) { + if (!Utils.fs.isSubPath(folderPath, folderPathOther)) + continue; + this.watchersClose(folderPathOther, filePath, false); + } + } + } + } + watchersLock(callback) { + return this._watchersLock.then(() => { + return this._watchersLock = new Promise(async (resolve) => { + await callback(); + resolve(); + }); + }); + } + watchersRestore() { + delete this._watchersRestoreTimeout; + const watchers = Object.entries(this._watchersRestorable); + this._watchersRestorable = {}; + for (const [targetPath, config] of watchers) { + this.watchPath(targetPath, config.options, config.handler); + } + } + async watcherAdd(config, baseWatcherHandler) { + const { folderPath } = config; + const configs = this._watchers[folderPath] = (this._watchers[folderPath] || []); + configs.push(config); + const watcherHandler = new WatcherHandler(this, config, baseWatcherHandler); + await watcherHandler.init(); + return watcherHandler; + } + watcherClose(config) { + config.watcher.close(); + const configs = this._watchers[config.folderPath]; + if (configs) { + const index = configs.indexOf(config); + configs.splice(index, 1); + if (!configs.length) { + delete this._watchers[config.folderPath]; + } + } + const rootPath = config.filePath || config.folderPath; + const isRoot = this._roots.has(rootPath); + if (isRoot) { + this._watchersRestorable[rootPath] = config; + if (!this._watchersRestoreTimeout) { + this._watchersRestoreTimeout = Utils.lang.defer(() => this.watchersRestore()); + } + } + } + watcherExists(folderPath, options, handler, filePath) { + const configsSibling = this._watchers[folderPath]; + if (!!configsSibling?.find(config => config.handler === handler && (!config.filePath || config.filePath === filePath) && config.options.ignore === options.ignore && !!config.options.native === !!options.native && (!options.recursive || config.options.recursive))) + return true; + let folderAncestorPath = path.dirname(folderPath); + for (let depth = 1; depth < Infinity; depth++) { + const configsAncestor = this._watchers[folderAncestorPath]; + if (!!configsAncestor?.find(config => (depth === 1 || (config.options.recursive && depth <= (config.options.depth ?? DEPTH))) && config.handler === handler && (!config.filePath || config.filePath === filePath) && config.options.ignore === options.ignore && !!config.options.native === !!options.native && (!options.recursive || (config.options.recursive && (HAS_NATIVE_RECURSION && config.options.native !== false))))) + return true; + if (!HAS_NATIVE_RECURSION) + break; // No other ancestor will possibly be found + const folderAncestorPathNext = path.dirname(folderPath); + if (folderAncestorPath === folderAncestorPathNext) + break; + folderAncestorPath = folderAncestorPathNext; + } + return false; + } + async watchDirectories(foldersPaths, options, handler, filePath, baseWatcherHandler) { + if (this.isClosed()) + return; + foldersPaths = Utils.lang.uniq(foldersPaths).sort(); + let watcherHandlerLast; + for (const folderPath of foldersPaths) { + if (this.isIgnored(folderPath, options.ignore)) + continue; + if (this.watcherExists(folderPath, options, handler, filePath)) + continue; + try { + const watcherOptions = (!options.recursive || (HAS_NATIVE_RECURSION && options.native !== false)) ? options : { ...options, recursive: false }; // Ensuring recursion is explicitly disabled if not available + const watcher = fs.watch(folderPath, watcherOptions); + const watcherConfig = { watcher, handler, options, folderPath, filePath }; + const watcherHandler = watcherHandlerLast = await this.watcherAdd(watcherConfig, baseWatcherHandler); + const isRoot = this._roots.has(filePath || folderPath); + if (isRoot) { + const parentOptions = { ...options, ignoreInitial: true, recursive: false }; // Ensuring only the parent folder is being watched + const parentFolderPath = path.dirname(folderPath); + const parentFilePath = folderPath; + await this.watchDirectories([parentFolderPath], parentOptions, handler, parentFilePath, watcherHandler); + //TODO: Watch parents recursively with the following code, which requires other things to be changed too though + // while ( true ) { + // await this.watchDirectories ( [parentFolderPath], parentOptions, handler, parentFilePath, watcherHandler ); + // const parentFolderPathNext = path.dirname ( parentFolderPath ); + // if ( parentFolderPath === parentFolderPathNext ) break; + // parentFilePath = parentFolderPath; + // parentFolderPath = parentFolderPathNext; + // } + } + } + catch (error) { + this.error(error); + } + } + return watcherHandlerLast; + } + async watchDirectory(folderPath, options, handler, filePath, baseWatcherHandler) { + if (this.isClosed()) + return; + if (this.isIgnored(folderPath, options.ignore)) + return; + if (!options.recursive || (HAS_NATIVE_RECURSION && options.native !== false)) { + return this.watchersLock(() => { + return this.watchDirectories([folderPath], options, handler, filePath, baseWatcherHandler); + }); + } + else { + options = { ...options, recursive: true }; // Ensuring recursion is explicitly enabled + const depth = options.depth ?? DEPTH; + const limit = options.limit ?? LIMIT; + const [folderSubPaths] = await Utils.fs.readdir(folderPath, options.ignore, depth, limit, this._closeSignal, options.readdirMap); + return this.watchersLock(async () => { + const watcherHandler = await this.watchDirectories([folderPath], options, handler, filePath, baseWatcherHandler); + if (folderSubPaths.length) { + const folderPathDepth = Utils.fs.getDepth(folderPath); + for (const folderSubPath of folderSubPaths) { + const folderSubPathDepth = Utils.fs.getDepth(folderSubPath); + const subDepth = Math.max(0, depth - (folderSubPathDepth - folderPathDepth)); + const subOptions = { ...options, depth: subDepth }; // Updating the maximum depth to account for depth of the sub path + await this.watchDirectories([folderSubPath], subOptions, handler, filePath, baseWatcherHandler || watcherHandler); + } + } + }); + } + } + async watchFileOnce(filePath, options, callback) { + if (this.isClosed()) + return; + options = { ...options, ignoreInitial: false }; // Ensuring initial events are detected too + if (this.subwatcherExists(filePath, options)) + return; + const config = { targetPath: filePath, options }; + const handler = (event, targetPath) => { + if (targetPath !== filePath) + return; + stop(); + callback(); + }; + const watcher = new Watcher(handler); + const start = () => { + this._subwatchers.add(config); + this.on(WatcherEvent.CLOSE, stop); // Ensuring the subwatcher is stopped on close + watcher.watchFile(filePath, options, handler); + }; + const stop = () => { + this._subwatchers.delete(config); + this.removeListener(WatcherEvent.CLOSE, stop); // Ensuring there are no leftover listeners + watcher.close(); + }; + return start(); + } + async watchFile(filePath, options, handler) { + if (this.isClosed()) + return; + if (this.isIgnored(filePath, options.ignore)) + return; + options = { ...options, recursive: false }; // Ensuring recursion is explicitly disabled + const folderPath = path.dirname(filePath); + return this.watchDirectory(folderPath, options, handler, filePath); + } + async watchPollingOnce(targetPath, options, callback) { + if (this.isClosed()) + return; + let isDone = false; + const poller = new WatcherPoller(); + const disposer = await this.watchPolling(targetPath, options, async () => { + if (isDone) + return; + const events = await poller.update(targetPath, options.pollingTimeout); + if (!events.length) + return; // Nothing actually changed, skipping + if (isDone) + return; // Another async callback has done the work already, skipping + isDone = true; + disposer(); + callback(); + }); + } + async watchPolling(targetPath, options, callback) { + if (this.isClosed()) + return Utils.lang.noop; + if (this.pollerExists(targetPath, options)) + return Utils.lang.noop; + const watcherOptions = { ...options, interval: options.pollingInterval ?? POLLING_INTERVAL }; // Ensuring a default interval is set + const config = { targetPath, options }; + const start = () => { + this._pollers.add(config); + this.on(WatcherEvent.CLOSE, stop); // Ensuring polling is stopped on close + fs.watchFile(targetPath, watcherOptions, callback); + }; + const stop = () => { + this._pollers.delete(config); + this.removeListener(WatcherEvent.CLOSE, stop); // Ensuring there are no leftover listeners + fs.unwatchFile(targetPath, callback); + }; + Utils.lang.attempt(start); + return () => Utils.lang.attempt(stop); + } + async watchUnknownChild(targetPath, options, handler) { + if (this.isClosed()) + return; + const watch = () => this.watchPath(targetPath, options, handler); + return this.watchFileOnce(targetPath, options, watch); + } + async watchUnknownTarget(targetPath, options, handler) { + if (this.isClosed()) + return; + const watch = () => this.watchPath(targetPath, options, handler); + return this.watchPollingOnce(targetPath, options, watch); + } + async watchPaths(targetPaths, options, handler) { + if (this.isClosed()) + return; + targetPaths = Utils.lang.uniq(targetPaths).sort(); + const isParallelizable = targetPaths.every((targetPath, index) => targetPaths.every((t, i) => i === index || !Utils.fs.isSubPath(targetPath, t))); // All paths are about separate subtrees, so we can start watching in parallel safely //TODO: Find parallelizable chunks rather than using an all or nothing approach + if (isParallelizable) { // Watching in parallel + await Promise.all(targetPaths.map(targetPath => { + return this.watchPath(targetPath, options, handler); + })); + } + else { // Watching serially + for (const targetPath of targetPaths) { + await this.watchPath(targetPath, options, handler); + } + } + } + async watchPath(targetPath, options, handler) { + if (this.isClosed()) + return; + targetPath = path.resolve(targetPath); + if (this.isIgnored(targetPath, options.ignore)) + return; + const stats = await Utils.fs.poll(targetPath, options.pollingTimeout); + if (!stats) { + const parentPath = path.dirname(targetPath); + const parentStats = await Utils.fs.poll(parentPath, options.pollingTimeout); + if (parentStats?.isDirectory()) { + return this.watchUnknownChild(targetPath, options, handler); + } + else { + return this.watchUnknownTarget(targetPath, options, handler); + } + } + else if (stats.isFile()) { + return this.watchFile(targetPath, options, handler); + } + else if (stats.isDirectory()) { + return this.watchDirectory(targetPath, options, handler); + } + else { + this.error(`"${targetPath}" is not supported`); + } + } + async watch(target, options, handler = Utils.lang.noop) { + if (Utils.lang.isFunction(target)) + return this.watch([], {}, target); + if (Utils.lang.isUndefined(target)) + return this.watch([], options, handler); + if (Utils.lang.isFunction(options)) + return this.watch(target, {}, options); + if (Utils.lang.isUndefined(options)) + return this.watch(target, {}, handler); + if (this.isClosed()) + return; + if (this.isReady()) + options.readdirMap = undefined; // Only usable before initialization + const targetPaths = Utils.lang.castArray(target); + targetPaths.forEach(targetPath => this._roots.add(targetPath)); + await this.watchPaths(targetPaths, options, handler); + if (this.isClosed()) + return; + if (handler !== Utils.lang.noop) { + this.on(WatcherEvent.ALL, handler); + } + options.readdirMap = undefined; // Only usable before initialization + this.ready(); + } +} +/* EXPORT */ +export default Watcher; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoid2F0Y2hlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3dhdGNoZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQ0EsWUFBWTtBQUVaLE9BQU8sRUFBQyxZQUFZLEVBQUMsTUFBTSxhQUFhLENBQUM7QUFDekMsT0FBTyxFQUFFLE1BQU0sU0FBUyxDQUFDO0FBQ3pCLE9BQU8sSUFBSSxNQUFNLFdBQVcsQ0FBQztBQUM3QixPQUFPLEVBQUMsS0FBSyxFQUFFLEtBQUssRUFBRSxvQkFBb0IsRUFBRSxnQkFBZ0IsRUFBQyxNQUFNLGdCQUFnQixDQUFDO0FBQ3BGLE9BQU8sRUFBQyxXQUFXLEVBQUUsWUFBWSxFQUFDLE1BQU0sWUFBWSxDQUFDO0FBQ3JELE9BQU8sS0FBSyxNQUFNLFlBQVksQ0FBQztBQUMvQixPQUFPLGNBQWMsTUFBTSxzQkFBc0IsQ0FBQztBQUNsRCxPQUFPLGFBQWEsTUFBTSxxQkFBcUIsQ0FBQztBQUNoRCxPQUFPLGFBQWEsTUFBTSxxQkFBcUIsQ0FBQztBQUdoRCxVQUFVO0FBRVYsTUFBTSxPQUFRLFNBQVEsWUFBWTtJQW9CaEMsaUJBQWlCO0lBRWpCLFlBQWMsTUFBZ0MsRUFBRSxPQUFrQyxFQUFFLE9BQWlCO1FBRW5HLEtBQUssRUFBRyxDQUFDO1FBRVQsSUFBSSxDQUFDLE9BQU8sR0FBRyxLQUFLLENBQUM7UUFDckIsSUFBSSxDQUFDLE1BQU0sR0FBRyxLQUFLLENBQUM7UUFDcEIsSUFBSSxDQUFDLGFBQWEsR0FBRyxJQUFJLGVBQWUsRUFBRyxDQUFDO1FBQzVDLElBQUksQ0FBQyxZQUFZLEdBQUcsSUFBSSxDQUFDLGFBQWEsQ0FBQyxNQUFNLENBQUM7UUFDOUMsSUFBSSxDQUFDLEVBQUUsQ0FBRyxZQUFZLENBQUMsS0FBSyxFQUFFLEdBQUcsRUFBRSxDQUFDLElBQUksQ0FBQyxhQUFhLENBQUMsS0FBSyxFQUFHLENBQUUsQ0FBQztRQUNsRSxJQUFJLENBQUMsVUFBVSxHQUFHLElBQUksT0FBTyxDQUFHLE9B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 \ No newline at end of file diff --git a/dist_ts/watcher_handler.d.ts b/dist_ts/watcher_handler.d.ts new file mode 100644 index 0000000..99976b2 --- /dev/null +++ b/dist_ts/watcher_handler.d.ts @@ -0,0 +1,36 @@ +/// +/// +import { FSTargetEvent } from './enums.js'; +import type Watcher from './watcher.js'; +import type { Event, FSWatcher, Handler, HandlerBatched, Path, WatcherOptions, WatcherConfig } from './types.js'; +declare class WatcherHandler { + base?: WatcherHandler; + watcher: Watcher; + handler: Handler; + handlerBatched: HandlerBatched; + fswatcher: FSWatcher; + options: WatcherOptions; + folderPath: Path; + filePath?: Path; + constructor(watcher: Watcher, config: WatcherConfig, base?: WatcherHandler); + _isSubRoot(targetPath: Path): boolean; + _makeHandlerBatched(delay?: number): (event: FSTargetEvent, targetPath?: Path, isInitial?: boolean) => Promise; + eventsDeduplicate(events: Event[]): Event[]; + eventsPopulate(targetPaths: Path[], events?: Event[], isInitial?: boolean): Promise; + eventsPopulateAddDir(targetPaths: Path[], targetPath: Path, events?: Event[], isInitial?: boolean): Promise; + eventsPopulateUnlinkDir(targetPaths: Path[], targetPath: Path, events?: Event[], isInitial?: boolean): Promise; + onTargetAdd(targetPath: Path): void; + onTargetAddDir(targetPath: Path): void; + onTargetChange(targetPath: Path): void; + onTargetUnlink(targetPath: Path): void; + onTargetUnlinkDir(targetPath: Path): void; + onTargetEvent(event: Event): void; + onTargetEvents(events: Event[]): void; + onWatcherEvent(event?: FSTargetEvent, targetPath?: Path, isInitial?: boolean): Promise; + onWatcherChange(event?: FSTargetEvent, targetName?: string | null): void; + onWatcherError(error: NodeJS.ErrnoException): void; + init(): Promise; + initWatcherEvents(): Promise; + initInitialEvents(): Promise; +} +export default WatcherHandler; diff --git a/dist_ts/watcher_handler.js b/dist_ts/watcher_handler.js new file mode 100644 index 0000000..677eda0 --- /dev/null +++ b/dist_ts/watcher_handler.js @@ -0,0 +1,249 @@ +/* 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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoid2F0Y2hlcl9oYW5kbGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvd2F0Y2hlcl9oYW5kbGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUNBLFlBQVk7QUFFWixPQUFPLElBQUksTUFBTSxXQUFXLENBQUM7QUFDN0IsT0FBTyxFQUFDLFFBQVEsRUFBRSxLQUFLLEVBQUUsS0FBSyxFQUFFLG9CQUFvQixFQUFFLFVBQVUsRUFBQyxNQUFNLGdCQUFnQixDQUFDO0FBQ3hGLE9BQU8sRUFBQyxhQUFhLEVBQUUsY0FBYyxFQUFFLFdBQVcsRUFBQyxNQUFNLFlBQVksQ0FBQztBQUN0RSxPQUFPLEtBQUssTUFBTSxZQUFZLENBQUM7QUFJL0IsVUFBVTtBQUVWLE1BQU0sY0FBYztJQWFsQixpQkFBaUI7SUFFakIsWUFBYyxPQUFnQixFQUFFLE1BQXFCLEVBQUUsSUFBcUI7UUFFMUUsSUFBSSxDQUFDLElBQUksR0FBRyxJQUFJLENBQUM7UUFDakIsSUFBSSxDQUFDLE9BQU8sR0FBRyxPQUFPLENBQUM7UUFDdkIsSUFBSSxDQUFDLE9BQU8sR0FBRyxNQUFNLENBQUMsT0FBTyxDQUFDO1FBQzlCLElBQUksQ0FBQyxTQUFTLEdBQUcsTUFBTSxDQUFDLE9BQU8sQ0FBQztRQUNoQyxJQUFJLENBQUMsT0FBTyxHQUFHLE1BQU0sQ0FBQyxPQUFPLENBQUM7UUFDOUIsSUFBSSxDQUFDLFVBQVUsR0FBRyxNQUFNLENBQUMsVUFBVSxDQUFDO1FBQ3BDLElBQUksQ0FBQyxRQUFRLEdBQUcsTUFBTSxDQUFDLFFBQVEsQ0FBQztRQUVoQyxJQUFJLENBQUMsY0FBYyxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsY0FBYyxDQUFDLElBQUksQ0FBRyxJQUFJLENBQUMsSUFBSSxDQUFFLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxtQkFBbUIsQ0FBRyxJQUFJLENBQUMsT0FBTyxDQUFDLFFBQVEsQ0FBRSxDQUFDLENBQUMsTUFBTTtJQUU1SSxDQUFDO0lBRUQsYUFBYTtJQUViLFVBQVUsQ0FBRyxVQUFnQjtRQUUzQixJQUFLLElBQUksQ0FBQyxRQUFRLEVBQUcsQ0FBQztZQUVwQixPQUFPLFVBQVUsS0FBSyxJQUFJLENBQUMsUUFBUSxDQUFDO1FBRXRDLENBQUM7YUFBTSxDQUFDO1lBRU4sT0FBTyxVQUFVLEtBQUssSUFBSSxDQUFDLFVBQVUsSUFBSSxLQUFLLENBQUMsRUFBRSxDQUFDLFNBQVMsQ0FBRyxJQUFJLENBQUMsVUFBVSxFQUFFLFVBQVUsQ0FBRSxDQUFDO1FBRTlGLENBQUM7SUFFSCxDQUFDO0lBRUQsbUJBQW1CLENBQUcsUUFBZ0IsUUFBUTtRQUU1QyxPQUFPLENBQUMsR0FBRyxFQUFFO1lBRVgsSUFBSSxJQUFJLEdBQUcsSUFBSSxDQUFDLE9BQU8sQ0FBQyxVQUFVLENBQUMsQ0FBQyxrRkFBa0Y7WUFDdEgsSUFBSSxRQUFRLEdBQVksRUFBRSxDQUFDO1lBQzNCLElBQUksUUFBUSxHQUFjLElBQUksR0FBRyxFQUFHLENBQUM7WUFFckMsTUFBTSxLQUFLLEdBQUcsS0FBSyxFQUFHLFFBQWlCLEVBQUUsUUFBbUIsRUFBa0IsRUFBRTtnQkFFOUUsTUFBTSxhQUFhLEdBQUcsSUFBSSxDQUFDLE9BQU8sQ0FBQyxhQUFhLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDO2dCQUNqRSxNQUFNLGFBQWEsR0FBRyxNQUFNLElBQUksQ0FBQyxjQUFjLENBQUUsQ0FBRSxHQUFHLFFBQVEsQ0FBRSxDQUFDLENBQUM7Z0JBQ2xFLE1BQU0sTUFBTSxHQUFHLElBQUksQ0FBQyxpQkFBaUIsQ0FBRSxDQUFFLEdBQUcsYUFBYSxFQUFFLEdBQUcsYUFBYSxDQUFFLENBQUMsQ0FBQztnQkFFL0UsSUFBSSxDQUFDLGNBQWMsQ0FBRyxNQUFNLENBQUUsQ0FBQztZQUVqQyxDQUFDLENBQUM7WUFFRixNQUFNLGNBQWMsR0FBRyxLQUFLLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBRyxHQUFHLEVBQUU7Z0JBRWhELElBQUssSUFBSSxDQUFDLE9BQU8sQ0FBQyxRQUFRLEVBQUc7b0JBQUcsT0FBTztnQkFFdkMsSUFBSSxHQUFHLEtBQUssQ0FBRyxRQUFRLEVBQUUsUUFBUSxDQUFFLENBQUM7Z0JBRXBDLFFBQVEsR0FBRyxFQUFFLENBQUM7Z0JBQ2QsUUFBUSxHQUFHLElBQUksR0FBRyxFQUFHLENBQUM7WUFFeEIsQ0FBQyxFQUFFLEtBQUssQ0FBRSxDQUFDO1lBRVgsT0FBTyxLQUFLLEVBQUcsS0FBb0IsRUFBRSxhQUFtQixFQUFFLEVBQUUsWUFBcUIsS0FBSyxFQUFrQixFQUFFO2dCQUV4RyxJQUFLLFNBQVMsRUFBRyxDQUFDLENBQUMsbUJBQW1CO29CQUVwQyxNQUFNLElBQUksQ0FBQyxjQUFjLENBQUcsQ0FBQyxVQUFVLENBQUMsRUFBRSxRQUFRLEVBQUUsSUFBSSxDQUFFLENBQUM7Z0JBRTdELENBQUM7cUJBQU0sQ0FBQyxDQUFDLGFBQWE7b0JBRXBCLFFBQVEsQ0FBQyxHQUFHLENBQUcsVUFBVSxDQUFFLENBQUM7Z0JBRTlCLENBQUM7Z0JBRUQsSUFBSSxDQUFDLElBQUksQ0FBRyxjQUFjLENBQUUsQ0FBQztZQUUvQixDQUFDLENBQUM7UUFFSixDQUFDLENBQUMsRUFBRSxDQUFDO0lBRVAsQ0FBQztJQUVELG1CQUFtQjtJQUVuQixpQkFBaUIsQ0FBRyxNQUFlO1FBRWpDLElBQUssTUFBTSxDQUFDLE1BQU0sR0FBRyxDQUFDO1lBQUcsT0FBTyxNQUFNLENBQUM7UUFFdkMsTUFBTSxnQkFBZ0IsR0FBOEIsRUFBRSxDQUFDO1FBRXZELE9BQU8sTUFBTSxDQUFDLE1BQU0sQ0FBWSxDQUFFLEdBQUcsRUFBRSxLQUFLLEVBQUcsRUFBRTtZQUUvQyxNQUFNLENBQUMsV0FBVyxFQUFFLFVBQVUsQ0FBQyxHQUFHLEtBQUssQ0FBQztZQUN4QyxNQUFNLGVBQWUsR0FBRyxnQkFBZ0IsQ0FBQyxVQUFVLENBQUMsQ0FBQztZQUVyRCxJQUFLLFdBQVcsS0FBSyxlQUFlO2dCQUFHLE9BQU8sR0FBRyxDQUFDLENBQUMsdUJBQXVCO1lBRTFFLElBQUssV0FBVyxLQUFLLFdBQVcsQ0FBQyxNQUFNLElBQUksZUFBZSxLQUFLLFdBQVcsQ0FBQyxHQUFHO2dCQUFHLE9BQU8sR0FBRyxDQUFDLENBQUMsaUNBQWlDO1lBRTlILGdCQUFnQixDQUFDLFVBQVUsQ0FBQyxHQUFHLFdBQVcsQ0FBQztZQUUzQyxHQUFHLENBQUMsSUFBSSxDQUFHLEtBQUssQ0FBRSxDQUFDO1lBRW5CLE9BQU8sR0FBRyxDQUFDO1FBRWIsQ0FBQyxFQUFFLEVBQUUsQ0FBRSxDQUFDO0lBRVYsQ0FBQztJQUVELEtBQUssQ0FBQyxjQUFjLENBQUcsV0FBbUIsRUFBRSxTQUFrQixFQUFFLEVBQUUsWUFBcUIsS0FBSztRQUUxRixNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUcsV0FBVyxDQUFDLEdBQUcsQ0FBRyxLQUFLLEVBQUMsVUFBVSxFQUFDLEVBQUU7WUFFdkQsTUFBTSxZQUFZLEdBQUcsTUFBTSxJQUFJLENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxNQUFNLENBQUcsVUFBVSxFQUFFLElBQUksQ0FBQyxPQUFPLENBQUMsY0FBYyxDQUFFLENBQUM7WUFFbkcsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFHLFlBQVksQ0FBQyxHQUFHLENBQUcsS0FBSyxFQUFDLEtBQUssRUFBQyxFQUFFO2dCQUVuRCxNQUFNLENBQUMsSUFBSSxDQUFFLENBQUUsS0FBSyxFQUFFLFVBQVUsQ0FBRSxDQUFDLENBQUM7Z0JBRXBDLElBQUssS0FBSyxLQUFLLFdBQVcsQ0FBQyxPQUFPLEVBQUcsQ0FBQztvQkFFcEMsTUFBTSxJQUFJLENBQUMsb0JBQW9CLENBQUcsV0FBVyxFQUFFLFVBQVUsRUFBRSxNQUFNLEVBQUUsU0FBUyxDQUFFLENBQUM7Z0JBRWpGLENBQUM7cUJBQU0sSUFBSyxLQUFLLEtBQUssV0FBVyxDQUFDLFVBQVUsRUFBRyxDQUFDO29CQUU5QyxNQUFNLElBQUksQ0FBQyx1QkFBdUIsQ0FBRyxXQUFXLEVBQUUsVUFBVSxFQUFFLE1BQU0sRUFBRSxTQUFTLENBQUUsQ0FBQztnQkFFcEYsQ0FBQztZQUVILENBQUMsQ0FBQyxDQUFDLENBQUM7UUFFTixDQUFDLENBQUMsQ0FBQyxDQUFDO1FBRUosT0FBTyxNQUFNLENBQUM7SUFFaEIsQ0FBQztJQUFBLENBQUM7SUFFRixLQUFLLENBQUMsb0JBQW9CLENBQUcsV0FBbUIsRUFBRSxVQUFnQixFQUFFLFNBQWtCLEVBQUUsRUFBRSxZQUFxQixLQUFLO1FBRWxILElBQUssU0FBUztZQUFHLE9BQU8sTUFBTSxDQUFDO1FBRS9CLE1BQU0sS0FBSyxHQUFHLElBQUksQ0FBQyxPQUFPLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLEtBQUssSUFBSSxLQUFLLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUcsQ0FBQyxFQUFFLElBQUksQ0FBQyxPQUFPLENBQUMsS0FBSyxJQUFJLEtBQUssQ0FBRSxDQUFDO1FBQ2pILE1BQU0sS0FBSyxHQUFHLElBQUksQ0FBQyxPQUFPLENBQUMsS0FBSyxJQUFJLEtBQUssQ0FBQztRQUMxQyxNQUFNLENBQUMsV0FBVyxFQUFFLEtBQUssQ0FBQyxHQUFHLE1BQU0sS0FBSyxDQUFDLEVBQUUsQ0FBQyxPQUFPLENBQUcsVUFBVSxFQUFFLElBQUksQ0FBQyxPQUFPLENBQUMsTUFBTSxFQUFFLEtBQUssRUFBRSxLQUFLLEVBQUUsSUFBSSxDQUFDLE9BQU8sQ0FBQyxZQUFZLENBQUUsQ0FBQztRQUNqSSxNQUFNLGNBQWMsR0FBRyxDQUFDLEdBQUcsV0FBVyxFQUFFLEdBQUcsS0FBSyxDQUFDLENBQUM7UUFFbEQsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFHLGNBQWMsQ0FBQyxHQUFHLENBQUcsYUFBYSxDQUFDLEVBQUU7WUFFdkQsSUFBSyxJQUFJLENBQUMsT0FBTyxDQUFDLFNBQVMsQ0FBRyxhQUFhLEVBQUUsSUFBSSxDQUFDLE9BQU8sQ0FBQyxNQUFNLENBQUU7Z0JBQUcsT0FBTztZQUU1RSxJQUFLLFdBQVcsQ0FBQyxRQUFRLENBQUcsYUFBYSxDQUFFO2dCQUFHLE9BQU87WUFFckQsT0FBTyxJQUFJLENBQUMsY0FBYyxDQUFHLENBQUMsYUFBYSxDQUFDLEVBQUUsTUFBTSxFQUFFLElBQUksQ0FBRSxDQUFDO1FBRS9ELENBQUMsQ0FBQyxDQUFDLENBQUM7UUFFSixPQUFPLE1BQU0sQ0FBQztJQUVoQixDQUFDO0lBRUQsS0FBSyxDQUFDLHVCQUF1QixDQUFHLFdBQW1CLEVBQUUsVUFBZ0IsRUFBRSxTQUFrQixFQUFFLEVBQUUsWUFBcUIsS0FBSztRQUVySCxJQUFLLFNBQVM7WUFBRyxPQUFPLE1BQU0sQ0FBQztRQUUvQixLQUFNLE1BQU0sZUFBZSxJQUFJLElBQUksQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDLEtBQUssQ0FBQyxJQUFJLEVBQUcsRUFBRyxDQUFDO1lBRW5FLElBQUssQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUFDLFNBQVMsQ0FBRyxVQUFVLEVBQUUsZUFBZSxDQUFFO2dCQUFHLFNBQVM7WUFFcEUsSUFBSyxXQUFXLENBQUMsUUFBUSxDQUFHLGVBQWUsQ0FBRTtnQkFBRyxTQUFTO1lBRXpELE1BQU0sSUFBSSxDQUFDLGNBQWMsQ0FBRyxDQUFDLGVBQWUsQ0FBQyxFQUFFLE1BQU0sRUFBRSxJQUFJLENBQUUsQ0FBQztRQUVoRSxDQUFDO1FBRUQsT0FBTyxNQUFNLENBQUM7SUFFaEIsQ0FBQztJQUVELG9CQUFvQjtJQUVwQixXQUFXLENBQUcsVUFBZ0I7UUFFNUIsSUFBSyxJQUFJLENBQUMsVUFBVSxDQUFHLFVBQVUsQ0FBRSxFQUFHLENBQUM7WUFFckMsSUFBSyxJQUFJLENBQUMsT0FBTyxDQUFDLGVBQWUsRUFBRyxDQUFDO2dCQUVuQyxJQUFJLENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxnQkFBZ0IsQ0FBRyxVQUFVLEVBQUUsSUFBSSxDQUFDLE9BQU8sQ0FBQyxhQUFhLENBQUUsQ0FBQztZQUVuRixDQUFDO2lCQUFNLENBQUM7Z0JBRU4sSUFBSSxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUcsV0FBVyxDQUFDLEdBQUcsRUFBRSxVQUFVLENBQUUsQ0FBQztZQUVyRCxDQUFDO1FBRUgsQ0FBQztJQUVILENBQUM7SUFFRCxjQUFjLENBQUcsVUFBZ0I7UUFFL0IsSUFBSyxVQUFVLEtBQUssSUFBSSxDQUFDLFVBQVUsSUFBSSxJQUFJLENBQUMsT0FBTyxDQUFDLFNBQVMsSUFBSSxDQUFFLENBQUMsb0JBQW9CLElBQUksSUFBSSxDQUFDLE9BQU8sQ0FBQyxNQUFNLEtBQUssS0FBSyxDQUFFLEVBQUcsQ0FBQztZQUU3SCxJQUFJLENBQUMsT0FBTyxDQUFDLGNBQWMsQ0FBRyxVQUFVLEVBQUUsSUFBSSxDQUFDLE9BQU8sRUFBRSxJQUFJLENBQUMsT0FBTyxFQUFFLFNBQVMsRUFBRSxJQUFJLENBQUMsSUFBSSxJQUFJLElBQUksQ0FBRSxDQUFDO1FBRXZHLENBQUM7UUFFRCxJQUFLLElBQUksQ0FBQyxVQUFVLENBQUcsVUFBVSxDQUFFLEVBQUcsQ0FBQztZQUVyQyxJQUFLLElBQUksQ0FBQyxPQUFPLENBQUMsZUFBZSxFQUFHLENBQUM7Z0JBRW5DLElBQUksQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDLG1CQUFtQixDQUFHLFVBQVUsRUFBRSxJQUFJLENBQUMsT0FBTyxDQUFDLGFBQWEsQ0FBRSxDQUFDO1lBRXRGLENBQUM7aUJBQU0sQ0FBQztnQkFFTixJQUFJLENBQUMsT0FBTyxDQUFDLEtBQUssQ0FBRyxXQUFXLENBQUMsT0FBTyxFQUFFLFVBQVUsQ0FBRSxDQUFDO1lBRXpELENBQUM7UUFFSCxDQUFDO0lBRUgsQ0FBQztJQUVELGNBQWMsQ0FBRyxVQUFnQjtRQUUvQixJQUFLLElBQUksQ0FBQyxVQUFVLENBQUcsVUFBVSxDQUFFLEVBQUcsQ0FBQztZQUVyQyxJQUFJLENBQUMsT0FBTyxDQUFDLEtBQUssQ0FBRyxXQUFXLENBQUMsTUFBTSxFQUFFLFVBQVUsQ0FBRSxDQUFDO1FBRXhELENBQUM7SUFFSCxDQUFDO0lBRUQsY0FBYyxDQUFHLFVBQWdCO1FBRS9CLElBQUksQ0FBQyxPQUFPLENBQUMsYUFBYSxDQUFHLElBQUksQ0FBQyxPQUFPLENBQUcsVUFBVSxDQUFFLEVBQUUsVUFBVSxFQUFFLEtBQUssQ0FBRSxDQUFDO1FBRTlFLElBQUssSUFBSSxDQUFDLFVBQVUsQ0FBRyxVQUFVLENBQUUsRUFBRyxDQUFDO1lBRXJDLElBQUssSUFBSSxDQUFDLE9BQU8sQ0FBQyxlQUFlLEVBQUcsQ0FBQztnQkFFbkMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxPQUFPLENBQUMsbUJBQW1CLENBQUcsVUFBVSxFQUFFLElBQUksQ0FBQyxPQUFPLENBQUMsYUFBYSxDQUFFLENBQUM7WUFFdEYsQ0FBQztpQkFBTSxDQUFDO2dCQUVOLElBQUksQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFHLFdBQVcsQ0FBQyxNQUFNLEVBQUUsVUFBVSxDQUFFLENBQUM7WUFFeEQsQ0FBQztRQUVILENBQUM7SUFFSCxDQUFDO0lBRUQsaUJBQWlCLENBQUcsVUFBZ0I7UUFFbEMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxhQUFhLENBQUcsSUFBSSxDQUFDLE9BQU8sQ0FBRyxVQUFVLENBQUUsRUFBRSxVQUFVLEVBQUUsS0FBSyxDQUFFLENBQUM7UUFFOUUsSUFBSSxDQUFDLE9BQU8sQ0FBQyxhQUFhLENBQUcsVUFBVSxDQUFFLENBQUM7UUFFMUMsSUFBSyxJQUFJLENBQUMsVUFBVSxDQUFHLFVBQVUsQ0FBRSxFQUFHLENBQUM7WUFFckMsSUFBSyxJQUFJLENBQUMsT0FBTyxDQUFDLGVBQWUsRUFBRyxDQUFDO2dCQUVuQyxJQUFJLENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxzQkFBc0IsQ0FBRyxVQUFVLEVBQUUsSUFBSSxDQUFDLE9BQU8sQ0FBQyxhQUFhLENBQUUsQ0FBQztZQUV6RixDQUFDO2lCQUFNLENBQUM7Z0JBRU4sSUFBSSxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUcsV0FBVyxDQUFDLFVBQVUsRUFBRSxVQUFVLENBQUUsQ0FBQztZQUU1RCxDQUFDO1FBRUgsQ0FBQztJQUVILENBQUM7SUFFRCxhQUFhLENBQUcsS0FBWTtRQUUxQixNQUFNLENBQUMsV0FBVyxFQUFFLFVBQVUsQ0FBQyxHQUFHLEtBQUssQ0FBQztRQUV4QyxJQUFLLFdBQVcsS0FBSyxXQUFXLENBQUMsR0FBRyxFQUFHLENBQUM7WUFFdEMsSUFBSSxDQUFDLFdBQVcsQ0FBRyxVQUFVLENBQUUsQ0FBQztRQUVsQyxDQUFDO2FBQU0sSUFBSyxXQUFXLEtBQUssV0FBVyxDQUFDLE9BQU8sRUFBRyxDQUFDO1lBRWpELElBQUksQ0FBQyxjQUFjLENBQUcsVUFBVSxDQUFFLENBQUM7UUFFckMsQ0FBQzthQUFNLElBQUssV0FBVyxLQUFLLFdBQVcsQ0FBQyxNQUFNLEVBQUcsQ0FBQztZQUVoRCxJQUFJLENBQUMsY0FBYyxDQUFHLFVBQVUsQ0FBRSxDQUFDO1FBRXJDLENBQUM7YUFBTSxJQUFLLFdBQVcsS0FBSyxXQUFXLENBQUMsTUFBTSxFQUFHLENBQUM7WUFFaEQsSUFBSSxDQUFDLGNBQWMsQ0FBRyxVQUFVLENBQUUsQ0FBQztRQUVyQyxDQUFDO2FBQU0sSUFBSyxXQUFXLEtBQUssV0FBVyxDQUFDLFVBQVUsRUFBRyxDQUFDO1lBRXBELElBQUksQ0FBQyxpQkFBaUIsQ0FBRyxVQUFVLENBQUUsQ0FBQztRQUV4QyxDQUFDO0lBRUgsQ0FBQztJQUVELGNBQWMsQ0FBRyxNQUFlO1FBRTlCLEtBQU0sTUFBTSxLQUFLLElBQUksTUFBTSxFQUFHLENBQUM7WUFFN0IsSUFBSSxDQUFDLGFBQWEsQ0FBRyxLQUFLLENBQUUsQ0FBQztRQUUvQixDQUFDO0lBRUgsQ0FBQztJQUVELGNBQWMsQ0FBRyxLQUFxQixFQUFFLFVBQWlCLEVBQUUsWUFBcUIsS0FBSztRQUVuRixPQUFPLElBQUksQ0FBQyxjQUFjLENBQUcsS0FBSyxFQUFFLFVBQVUsRUFBRSxTQUFTLENBQUUsQ0FBQztJQUU5RCxDQUFDO0lBRUQsZUFBZSxDQUFHLFFBQXVCLGFBQWEsQ0FBQyxNQUFNLEVBQUUsVUFBMEI7UUFFdkYsSUFBSyxJQUFJLENBQUMsT0FBTyxDQUFDLFFBQVEsRUFBRztZQUFHLE9BQU87UUFFdkMsTUFBTSxVQUFVLEdBQUcsSUFBSSxDQUFDLE9BQU8sQ0FBRyxJQUFJLENBQUMsVUFBVSxFQUFFLFVBQVUsSUFBSSxFQUFFLENBQUUsQ0FBQztRQUV0RSxJQUFLLElBQUksQ0FBQyxRQUFRLElBQUksVUFBVSxLQUFLLElBQUksQ0FBQyxVQUFVLElBQUksVUFBVSxLQUFLLElBQUksQ0FBQyxRQUFRO1lBQUcsT0FBTztRQUU5RixJQUFLLElBQUksQ0FBQyxPQUFPLENBQUMsU0FBUyxDQUFHLFVBQVUsRUFBRSxJQUFJLENBQUMsT0FBTyxDQUFDLE1BQU0sQ0FBRTtZQUFHLE9BQU87UUFFekUsSUFBSSxDQUFDLGNBQWMsQ0FBRyxLQUFLLEVBQUUsVUFBVSxDQUFFLENBQUM7SUFFNUMsQ0FBQztJQUVELGNBQWMsQ0FBRyxLQUE0QjtRQUUzQyxJQUFLLFVBQVUsSUFBSSxLQUFLLENBQUMsSUFBSSxLQUFLLE9BQU8sRUFBRyxDQUFDLENBQUMsMkNBQTJDO1lBRXZGLElBQUksQ0FBQyxlQUFlLENBQUcsYUFBYSxDQUFDLE1BQU0sRUFBRSxFQUFFLENBQUUsQ0FBQztRQUVwRCxDQUFDO2FBQU0sQ0FBQztZQUVOLElBQUksQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFHLEtBQUssQ0FBRSxDQUFDO1FBRS9CLENBQUM7SUFFSCxDQUFDO0lBRUQsU0FBUztJQUVULEtBQUssQ0FBQyxJQUFJO1FBRVIsTUFBTSxJQUFJLENBQUMsaUJBQWlCLEVBQUcsQ0FBQztRQUNoQyxNQUFNLElBQUksQ0FBQyxpQkFBaUIsRUFBRyxDQUFDO0lBRWxDLENBQUM7SUFFRCxLQUFLLENBQUMsaUJBQWlCO1FBRXJCLE1BQU0sUUFBUSxHQUFHLElBQUksQ0FBQyxlQUFlLENBQUMsSUFBSSxDQUFHLElBQUksQ0FBRSxDQUFDO1FBRXBELElBQUksQ0FBQyxTQUFTLENBQUMsRUFBRSxDQUFHLGNBQWMsQ0FBQyxNQUFNLEVBQUUsUUFBUSxDQUFFLENBQUM7UUFFdEQsTUFBTSxPQUFPLEdBQUcsSUFBSSxDQUFDLGNBQWMsQ0FBQyxJQUFJLENBQUcsSUFBSSxDQUFFLENBQUM7UUFFbEQsSUFBSSxDQUFDLFNBQVMsQ0FBQyxFQUFFLENBQUcsY0FBYyxDQUFDLEtBQUssRUFBRSxPQUFPLENBQUUsQ0FBQztJQUV0RCxDQUFDO0lBRUQsS0FBSyxDQUFDLGlCQUFpQjtRQUVyQixNQUFNLFNBQVMsR0FBRyxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsT0FBTyxFQUFHLENBQUMsQ0FBQyw2REFBNkQ7UUFFekcsSUFBSyxJQUFJLENBQUMsUUFBUSxFQUFHLENBQUMsQ0FBQyxzQkFBc0I7WUFFM0MsSUFBSyxJQUFJLENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFHLElBQUksQ0FBQyxRQUFRLENBQUU7Z0JBQUcsT0FBTyxDQUFDLGlCQUFpQjtZQUVqRixNQUFNLElBQUksQ0FBQyxjQUFjLENBQUcsYUFBYSxDQUFDLE1BQU0sRUFBRSxJQUFJLENBQUMsUUFBUSxFQUFFLFNBQVMsQ0FBRSxDQUFDO1FBRS9FLENBQUM7YUFBTSxDQUFDLENBQUMseUJBQXlCO1lBRWhDLE1BQU0sS0FBSyxHQUFHLElBQUksQ0FBQyxPQUFPLENBQUMsU0FBUyxJQUFJLENBQUUsb0JBQW9CLElBQUksSUFBSSxDQUFDLE9BQU8sQ0FBQyxNQUFNLEtBQUssS0FBSyxDQUFFLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsS0FBSyxJQUFJLEtBQUssQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBRyxDQUFDLEVBQUUsSUFBSSxDQUFDLE9BQU8sQ0FBQyxLQUFLLElBQUksS0FBSyxDQUFFLENBQUM7WUFDOUssTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLE9BQU8sQ0FBQyxLQUFLLElBQUksS0FBSyxDQUFDO1lBQzFDLE1BQU0sQ0FBQyxXQUFXLEVBQUUsS0FBSyxDQUFDLEdBQUcsTUFBTSxLQUFLLENBQUMsRUFBRSxDQUFDLE9BQU8sQ0FBRyxJQUFJLENBQUMsVUFBVSxFQUFFLElBQUksQ0FBQyxPQUFPLENBQUMsTUFBTSxFQUFFLEtBQUssRUFBRSxLQUFLLEVBQUUsSUFBSSxDQUFDLE9BQU8sQ0FBQyxZQUFZLEVBQUUsSUFBSSxDQUFDLE9BQU8sQ0FBQyxVQUFVLENBQUUsQ0FBQztZQUMvSixNQUFNLFdBQVcsR0FBRyxDQUFDLElBQUksQ0FBQyxVQUFVLEVBQUUsR0FBRyxXQUFXLEVBQUUsR0FBRyxLQUFLLENBQUMsQ0FBQztZQUVoRSxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUcsV0FBVyxDQUFDLEdBQUcsQ0FBRyxVQUFVLENBQUMsRUFBRTtnQkFFakQsSUFBSyxJQUFJLENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFHLFVBQVUsQ0FBRTtvQkFBRyxPQUFPLENBQUMsaUJBQWlCO2dCQUU5RSxJQUFLLElBQUksQ0FBQyxPQUFPLENBQUMsU0FBUyxDQUFHLFVBQVUsRUFBRSxJQUFJLENBQUMsT0FBTyxDQUFDLE1BQU0sQ0FBRTtvQkFBRyxPQUFPO2dCQUV6RSxPQUFPLElBQUksQ0FBQyxjQUFjLENBQUcsYUFBYSxDQUFDLE1BQU0sRUFBRSxVQUFVLEVBQUUsU0FBUyxDQUFFLENBQUM7WUFFN0UsQ0FBQyxDQUFDLENBQUMsQ0FBQztRQUVOLENBQUM7SUFFSCxDQUFDO0NBRUY7QUFFRCxZQUFZO0FBRVosZUFBZSxjQUFjLENBQUMifQ== \ No newline at end of file diff --git a/dist_ts/watcher_locker.d.ts b/dist_ts/watcher_locker.d.ts new file mode 100644 index 0000000..dd31f71 --- /dev/null +++ b/dist_ts/watcher_locker.d.ts @@ -0,0 +1,32 @@ +import { TargetEvent } from './enums.js'; +import type Watcher from './watcher.js'; +import type { Path, LocksAdd, LocksUnlink, LocksPair, LockConfig } from './types.js'; +declare class WatcherLocker { + _locksAdd: LocksAdd; + _locksAddDir: LocksAdd; + _locksUnlink: LocksUnlink; + _locksUnlinkDir: LocksUnlink; + _locksDir: LocksPair; + _locksFile: LocksPair; + _watcher: Watcher; + static DIR_EVENTS: { + readonly add: TargetEvent.ADD_DIR; + readonly rename: TargetEvent.RENAME_DIR; + readonly unlink: TargetEvent.UNLINK_DIR; + }; + static FILE_EVENTS: { + readonly add: TargetEvent.ADD; + readonly change: TargetEvent.CHANGE; + readonly rename: TargetEvent.RENAME; + readonly unlink: TargetEvent.UNLINK; + }; + constructor(watcher: Watcher); + getLockAdd(config: LockConfig, timeout?: number): void; + getLockUnlink(config: LockConfig, timeout?: number): void; + getLockTargetAdd(targetPath: Path, timeout?: number): void; + getLockTargetAddDir(targetPath: Path, timeout?: number): void; + getLockTargetUnlink(targetPath: Path, timeout?: number): void; + getLockTargetUnlinkDir(targetPath: Path, timeout?: number): void; + reset(): void; +} +export default WatcherLocker; diff --git a/dist_ts/watcher_locker.js b/dist_ts/watcher_locker.js new file mode 100644 index 0000000..073116c --- /dev/null +++ b/dist_ts/watcher_locker.js @@ -0,0 +1,140 @@ +/* IMPORT */ +import { RENAME_TIMEOUT } from './constants.js'; +import { FileType, TargetEvent } from './enums.js'; +import Utils from './utils.js'; +import WatcherLocksResolver from './watcher_locks_resolver.js'; +/* MAIN */ +//TODO: Use a better name for this thing, maybe "RenameDetector" +class WatcherLocker { + static { this.DIR_EVENTS = { + add: TargetEvent.ADD_DIR, + rename: TargetEvent.RENAME_DIR, + unlink: TargetEvent.UNLINK_DIR + }; } + static { this.FILE_EVENTS = { + add: TargetEvent.ADD, + change: TargetEvent.CHANGE, + rename: TargetEvent.RENAME, + unlink: TargetEvent.UNLINK + }; } + /* CONSTRUCTOR */ + constructor(watcher) { + this._watcher = watcher; + this.reset(); + } + /* API */ + getLockAdd(config, timeout = RENAME_TIMEOUT) { + const { ino, targetPath, events, locks } = config; + const emit = () => { + const otherPath = this._watcher._poller.paths.find(ino || -1, path => path !== targetPath); // Maybe this is actually a rename in a case-insensitive filesystem + if (otherPath && otherPath !== targetPath) { + if (Utils.fs.getRealPath(targetPath, true) === otherPath) + return; //TODO: This seems a little too special-casey + this._watcher.event(events.rename, otherPath, targetPath); + } + else { + this._watcher.event(events.add, targetPath); + } + }; + if (!ino) + return emit(); + const cleanup = () => { + locks.add.delete(ino); + WatcherLocksResolver.remove(free); + }; + const free = () => { + cleanup(); + emit(); + }; + WatcherLocksResolver.add(free, timeout); + const resolve = () => { + const unlink = locks.unlink.get(ino); + if (!unlink) + return; // No matching "unlink" lock found, skipping + cleanup(); + const targetPathPrev = unlink(); + if (targetPath === targetPathPrev) { + if (events.change) { + if (this._watcher._poller.stats.has(targetPath)) { + this._watcher.event(events.change, targetPath); + } + } + } + else { + this._watcher.event(events.rename, targetPathPrev, targetPath); + } + }; + locks.add.set(ino, resolve); + resolve(); + } + getLockUnlink(config, timeout = RENAME_TIMEOUT) { + const { ino, targetPath, events, locks } = config; + const emit = () => { + this._watcher.event(events.unlink, targetPath); + }; + if (!ino) + return emit(); + const cleanup = () => { + locks.unlink.delete(ino); + WatcherLocksResolver.remove(free); + }; + const free = () => { + cleanup(); + emit(); + }; + WatcherLocksResolver.add(free, timeout); + const overridden = () => { + cleanup(); + return targetPath; + }; + locks.unlink.set(ino, overridden); + locks.add.get(ino)?.(); + } + getLockTargetAdd(targetPath, timeout) { + const ino = this._watcher._poller.getIno(targetPath, TargetEvent.ADD, FileType.FILE); + return this.getLockAdd({ + ino, + targetPath, + events: WatcherLocker.FILE_EVENTS, + locks: this._locksFile + }, timeout); + } + getLockTargetAddDir(targetPath, timeout) { + const ino = this._watcher._poller.getIno(targetPath, TargetEvent.ADD_DIR, FileType.DIR); + return this.getLockAdd({ + ino, + targetPath, + events: WatcherLocker.DIR_EVENTS, + locks: this._locksDir + }, timeout); + } + getLockTargetUnlink(targetPath, timeout) { + const ino = this._watcher._poller.getIno(targetPath, TargetEvent.UNLINK, FileType.FILE); + return this.getLockUnlink({ + ino, + targetPath, + events: WatcherLocker.FILE_EVENTS, + locks: this._locksFile + }, timeout); + } + getLockTargetUnlinkDir(targetPath, timeout) { + const ino = this._watcher._poller.getIno(targetPath, TargetEvent.UNLINK_DIR, FileType.DIR); + return this.getLockUnlink({ + ino, + targetPath, + events: WatcherLocker.DIR_EVENTS, + locks: this._locksDir + }, timeout); + } + reset() { + this._locksAdd = new Map(); + this._locksAddDir = new Map(); + this._locksUnlink = new Map(); + this._locksUnlinkDir = new Map(); + this._locksDir = { add: this._locksAddDir, unlink: this._locksUnlinkDir }; + this._locksFile = { add: this._locksAdd, unlink: this._locksUnlink }; + } +} +/* EXPORT */ +export default WatcherLocker; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoid2F0Y2hlcl9sb2NrZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy93YXRjaGVyX2xvY2tlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFDQSxZQUFZO0FBRVosT0FBTyxFQUFDLGNBQWMsRUFBQyxNQUFNLGdCQUFnQixDQUFDO0FBQzlDLE9BQU8sRUFBQyxRQUFRLEVBQUUsV0FBVyxFQUFDLE1BQU0sWUFBWSxDQUFDO0FBQ2pELE9BQU8sS0FBSyxNQUFNLFlBQVksQ0FBQztBQUMvQixPQUFPLG9CQUFvQixNQUFNLDZCQUE2QixDQUFDO0FBSS9ELFVBQVU7QUFFVixnRUFBZ0U7QUFFaEUsTUFBTSxhQUFhO2FBWVYsZUFBVSxHQUFXO1FBQzFCLEdBQUcsRUFBRSxXQUFXLENBQUMsT0FBTztRQUN4QixNQUFNLEVBQUUsV0FBVyxDQUFDLFVBQVU7UUFDOUIsTUFBTSxFQUFFLFdBQVcsQ0FBQyxVQUFVO0tBQy9CLENBQUM7YUFFSyxnQkFBVyxHQUFXO1FBQzNCLEdBQUcsRUFBRSxXQUFXLENBQUMsR0FBRztRQUNwQixNQUFNLEVBQUUsV0FBVyxDQUFDLE1BQU07UUFDMUIsTUFBTSxFQUFFLFdBQVcsQ0FBQyxNQUFNO1FBQzFCLE1BQU0sRUFBRSxXQUFXLENBQUMsTUFBTTtLQUMzQixDQUFDO0lBRUYsaUJBQWlCO0lBRWpCLFlBQWMsT0FBZ0I7UUFFNUIsSUFBSSxDQUFDLFFBQVEsR0FBRyxPQUFPLENBQUM7UUFFeEIsSUFBSSxDQUFDLEtBQUssRUFBRyxDQUFDO0lBRWhCLENBQUM7SUFFRCxTQUFTO0lBRVQsVUFBVSxDQUFHLE1BQWtCLEVBQUUsVUFBa0IsY0FBYztRQUUvRCxNQUFNLEVBQUMsR0FBRyxFQUFFLFVBQVUsRUFBRSxNQUFNLEVBQUUsS0FBSyxFQUFDLEdBQUcsTUFBTSxDQUFDO1FBRWhELE1BQU0sSUFBSSxHQUFHLEdBQVMsRUFBRTtZQUN0QixNQUFNLFNBQVMsR0FBRyxJQUFJLENBQUMsUUFBUSxDQUFDLE9BQU8sQ0FB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 \ No newline at end of file diff --git a/dist_ts/watcher_locks_resolver.d.ts b/dist_ts/watcher_locks_resolver.d.ts new file mode 100644 index 0000000..facb2a0 --- /dev/null +++ b/dist_ts/watcher_locks_resolver.d.ts @@ -0,0 +1,12 @@ +/// +declare const WatcherLocksResolver: { + interval: number; + intervalId: NodeJS.Timeout; + fns: Map; + init: () => void; + reset: () => void; + add: (fn: Function, timeout: number) => void; + remove: (fn: Function) => void; + resolve: () => void; +}; +export default WatcherLocksResolver; diff --git a/dist_ts/watcher_locks_resolver.js b/dist_ts/watcher_locks_resolver.js new file mode 100644 index 0000000..a285e1b --- /dev/null +++ b/dist_ts/watcher_locks_resolver.js @@ -0,0 +1,43 @@ +/* MAIN */ +// Registering a single interval scales much better than registering N timeouts +// Timeouts are respected within the interval margin +const WatcherLocksResolver = { + /* VARIABLES */ + interval: 100, + intervalId: undefined, + fns: new Map(), + /* LIFECYCLE API */ + init: () => { + if (WatcherLocksResolver.intervalId) + return; + WatcherLocksResolver.intervalId = setInterval(WatcherLocksResolver.resolve, WatcherLocksResolver.interval); + }, + reset: () => { + if (!WatcherLocksResolver.intervalId) + return; + clearInterval(WatcherLocksResolver.intervalId); + delete WatcherLocksResolver.intervalId; + }, + /* API */ + add: (fn, timeout) => { + WatcherLocksResolver.fns.set(fn, Date.now() + timeout); + WatcherLocksResolver.init(); + }, + remove: (fn) => { + WatcherLocksResolver.fns.delete(fn); + }, + resolve: () => { + if (!WatcherLocksResolver.fns.size) + return WatcherLocksResolver.reset(); + const now = Date.now(); + for (const [fn, timestamp] of WatcherLocksResolver.fns) { + if (timestamp >= now) + continue; // We should still wait some more for this + WatcherLocksResolver.remove(fn); + fn(); + } + } +}; +/* EXPORT */ +export default WatcherLocksResolver; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoid2F0Y2hlcl9sb2Nrc19yZXNvbHZlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3dhdGNoZXJfbG9ja3NfcmVzb2x2ZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQ0EsVUFBVTtBQUVWLCtFQUErRTtBQUMvRSxvREFBb0Q7QUFFcEQsTUFBTSxvQkFBb0IsR0FBRztJQUUzQixlQUFlO0lBRWYsUUFBUSxFQUFFLEdBQUc7SUFDYixVQUFVLEVBQUUsU0FBdUM7SUFDbkQsR0FBRyxFQUFFLElBQUksR0FBRyxFQUFxQjtJQUVqQyxtQkFBbUI7SUFFbkIsSUFBSSxFQUFFLEdBQVMsRUFBRTtRQUVmLElBQUssb0JBQW9CLENBQUMsVUFBVTtZQUFHLE9BQU87UUFFOUMsb0JBQW9CLENBQUMsVUFBVSxHQUFHLFdBQVcsQ0FBRyxvQkFBb0IsQ0FBQyxPQUFPLEVBQUUsb0JBQW9CLENBQUMsUUFBUSxDQUFFLENBQUM7SUFFaEgsQ0FBQztJQUVELEtBQUssRUFBRSxHQUFTLEVBQUU7UUFFaEIsSUFBSyxDQUFDLG9CQUFvQixDQUFDLFVBQVU7WUFBRyxPQUFPO1FBRS9DLGFBQWEsQ0FBRyxvQkFBb0IsQ0FBQyxVQUFVLENBQUUsQ0FBQztRQUVsRCxPQUFPLG9CQUFvQixDQUFDLFVBQVUsQ0FBQztJQUV6QyxDQUFDO0lBRUQsU0FBUztJQUVULEdBQUcsRUFBRSxDQUFFLEVBQVksRUFBRSxPQUFlLEVBQVMsRUFBRTtRQUU3QyxvQkFBb0IsQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFHLEVBQUUsRUFBRSxJQUFJLENBQUMsR0FBRyxFQUFHLEdBQUcsT0FBTyxDQUFFLENBQUM7UUFFM0Qsb0JBQW9CLENBQUMsSUFBSSxFQUFHLENBQUM7SUFFL0IsQ0FBQztJQUVELE1BQU0sRUFBRSxDQUFFLEVBQVksRUFBUyxFQUFFO1FBRS9CLG9CQUFvQixDQUFDLEdBQUcsQ0FBQyxNQUFNLENBQUcsRUFBRSxDQUFFLENBQUM7SUFFekMsQ0FBQztJQUVELE9BQU8sRUFBRSxHQUFTLEVBQUU7UUFFbEIsSUFBSyxDQUFDLG9CQUFvQixDQUFDLEdBQUcsQ0FBQyxJQUFJO1lBQUcsT0FBTyxvQkFBb0IsQ0FBQyxLQUFLLEVBQUcsQ0FBQztRQUUzRSxNQUFNLEdBQUcsR0FBRyxJQUFJLENBQUMsR0FBRyxFQUFHLENBQUM7UUFFeEIsS0FBTSxNQUFNLENBQUMsRUFBRSxFQUFFLFNBQVMsQ0FBQyxJQUFJLG9CQUFvQixDQUFDLEdBQUcsRUFBRyxDQUFDO1lBRXpELElBQUssU0FBUyxJQUFJLEdBQUc7Z0JBQUcsU0FBUyxDQUFDLDBDQUEwQztZQUU1RSxvQkFBb0IsQ0FBQyxNQUFNLENBQUcsRUFBRSxDQUFFLENBQUM7WUFFbkMsRUFBRSxFQUFHLENBQUM7UUFFUixDQUFDO0lBRUgsQ0FBQztDQUVGLENBQUM7QUFFRixZQUFZO0FBRVosZUFBZSxvQkFBb0IsQ0FBQyJ9 \ No newline at end of file diff --git a/dist_ts/watcher_poller.d.ts b/dist_ts/watcher_poller.d.ts new file mode 100644 index 0000000..a2e2845 --- /dev/null +++ b/dist_ts/watcher_poller.d.ts @@ -0,0 +1,17 @@ +import { FileType, TargetEvent } from './enums.js'; +import LazyMapSet from './lazy_map_set.js'; +import WatcherStats from './watcher_stats.js'; +import type { INO, Path } from './types.js'; +declare class WatcherPoller { + inos: Partial>>; + paths: LazyMapSet; + stats: Map; + getIno(targetPath: Path, event: TargetEvent, type?: FileType): INO | undefined; + getStats(targetPath: Path): WatcherStats | undefined; + poll(targetPath: Path, timeout?: number): Promise; + reset(): void; + update(targetPath: Path, timeout?: number): Promise; + updateIno(targetPath: Path, event: TargetEvent, stats: WatcherStats): void; + updateStats(targetPath: Path, stats?: WatcherStats): void; +} +export default WatcherPoller; diff --git a/dist_ts/watcher_poller.js b/dist_ts/watcher_poller.js new file mode 100644 index 0000000..fb53e2b --- /dev/null +++ b/dist_ts/watcher_poller.js @@ -0,0 +1,117 @@ +/* IMPORT */ +import { FileType, TargetEvent } from './enums.js'; +import LazyMapSet from './lazy_map_set.js'; +import Utils from './utils.js'; +import WatcherStats from './watcher_stats.js'; +/* MAIN */ +class WatcherPoller { + constructor() { + /* VARIABLES */ + this.inos = {}; + this.paths = new LazyMapSet(); + this.stats = new Map(); + } + /* API */ + getIno(targetPath, event, type) { + const inos = this.inos[event]; + if (!inos) + return; + const ino = inos[targetPath]; + if (!ino) + return; + if (type && ino[1] !== type) + return; + return ino[0]; + } + getStats(targetPath) { + return this.stats.get(targetPath); + } + async poll(targetPath, timeout) { + const stats = await Utils.fs.poll(targetPath, timeout); + if (!stats) + return; + const isSupported = stats.isFile() || stats.isDirectory(); + if (!isSupported) + return; + return new WatcherStats(stats); + } + reset() { + this.inos = {}; + this.paths = new LazyMapSet(); + this.stats = new Map(); + } + async update(targetPath, timeout) { + const prev = this.getStats(targetPath); + const next = await this.poll(targetPath, timeout); + this.updateStats(targetPath, next); + if (!prev && next) { + if (next.isFile()) { + this.updateIno(targetPath, TargetEvent.ADD, next); + return [TargetEvent.ADD]; + } + if (next.isDirectory()) { + this.updateIno(targetPath, TargetEvent.ADD_DIR, next); + return [TargetEvent.ADD_DIR]; + } + } + else if (prev && !next) { + if (prev.isFile()) { + this.updateIno(targetPath, TargetEvent.UNLINK, prev); + return [TargetEvent.UNLINK]; + } + if (prev.isDirectory()) { + this.updateIno(targetPath, TargetEvent.UNLINK_DIR, prev); + return [TargetEvent.UNLINK_DIR]; + } + } + else if (prev && next) { + if (prev.isFile()) { + if (next.isFile()) { + if (prev.ino === next.ino && !prev.size && !next.size) + return []; // Same path, same content and same file, nothing actually changed + this.updateIno(targetPath, TargetEvent.CHANGE, next); + return [TargetEvent.CHANGE]; + } + if (next.isDirectory()) { + this.updateIno(targetPath, TargetEvent.UNLINK, prev); + this.updateIno(targetPath, TargetEvent.ADD_DIR, next); + return [TargetEvent.UNLINK, TargetEvent.ADD_DIR]; + } + } + else if (prev.isDirectory()) { + if (next.isFile()) { + this.updateIno(targetPath, TargetEvent.UNLINK_DIR, prev); + this.updateIno(targetPath, TargetEvent.ADD, next); + return [TargetEvent.UNLINK_DIR, TargetEvent.ADD]; + } + if (next.isDirectory()) { + if (prev.ino === next.ino) + return []; // Same path and same directory, nothing actually changed + this.updateIno(targetPath, TargetEvent.UNLINK_DIR, prev); + this.updateIno(targetPath, TargetEvent.ADD_DIR, next); + return [TargetEvent.UNLINK_DIR, TargetEvent.ADD_DIR]; + } + } + } + return []; + } + updateIno(targetPath, event, stats) { + const inos = this.inos[event] = this.inos[event] || (this.inos[event] = {}); + const type = stats.isFile() ? FileType.FILE : FileType.DIR; + inos[targetPath] = [stats.ino, type]; + } + updateStats(targetPath, stats) { + if (stats) { + this.paths.set(stats.ino, targetPath); + this.stats.set(targetPath, stats); + } + else { + const ino = this.stats.get(targetPath)?.ino || -1; + this.paths.delete(ino, targetPath); + this.stats.delete(targetPath); + } + } +} +/* EXPORT */ +export default WatcherPoller; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoid2F0Y2hlcl9wb2xsZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy93YXRjaGVyX3BvbGxlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFDQSxZQUFZO0FBRVosT0FBTyxFQUFDLFFBQVEsRUFBRSxXQUFXLEVBQUMsTUFBTSxZQUFZLENBQUM7QUFDakQsT0FBTyxVQUFVLE1BQU0sbUJBQW1CLENBQUM7QUFDM0MsT0FBTyxLQUFLLE1BQU0sWUFBWSxDQUFDO0FBQy9CLE9BQU8sWUFBWSxNQUFNLG9CQUFvQixDQUFDO0FBRzlDLFVBQVU7QUFFVixNQUFNLGFBQWE7SUFBbkI7UUFFRSxlQUFlO1FBRWYsU0FBSSxHQUFnRSxFQUFFLENBQUM7UUFDdkUsVUFBSyxHQUEwQixJQUFJLFVBQVUsRUFBRyxDQUFDO1FBQ2pELFVBQUssR0FBNEIsSUFBSSxHQUFHLEVBQUcsQ0FBQztJQTJLOUMsQ0FBQztJQXpLQyxTQUFTO0lBRVQsTUFBTSxDQUFHLFVBQWdCLEVBQUUsS0FBa0IsRUFBRSxJQUFlO1FBRTVELE1BQU0sSUFBSSxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUM7UUFFOUIsSUFBSyxDQUFDLElBQUk7WUFBRyxPQUFPO1FBRXBCLE1BQU0sR0FBRyxHQUFHLElBQUksQ0FBQyxVQUFVLENBQUMsQ0FBQztRQUU3QixJQUFLLENBQUMsR0FBRztZQUFHLE9BQU87UUFFbkIsSUFBSyxJQUFJLElBQUksR0FBRyxDQUFDLENBQUMsQ0FBQyxLQUFLLElBQUk7WUFBRyxPQUFPO1FBRXRDLE9BQU8sR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDO0lBRWhCLENBQUM7SUFFRCxRQUFRLENBQUcsVUFBZ0I7UUFFekIsT0FBTyxJQUFJLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBRyxVQUFVLENBQUUsQ0FBQztJQUV2QyxDQUFDO0lBRUQsS0FBSyxDQUFDLElBQUksQ0FBRyxVQUFnQixFQUFFLE9BQWdCO1FBRTdDLE1BQU0sS0FBSyxHQUFHLE1BQU0sS0FBSyxDQUFDLEVBQUUsQ0FBQyxJQUFJLENBQUcsVUFBVSxFQUFFLE9BQU8sQ0FBRSxDQUFDO1FBRTFELElBQUssQ0FBQyxLQUFLO1lBQUcsT0FBTztRQUVyQixNQUFNLFdBQVcsR0FBRyxLQUFLLENBQUMsTUFBTSxFQUFHLElBQUksS0FBSyxDQUFDLFdBQVcsRUFBRyxDQUFDO1FBRTVELElBQUssQ0FBQyxXQUFXO1lBQUcsT0FBTztRQUUzQixPQUFPLElBQUksWUFBWSxDQUFHLEtBQUssQ0FBRSxDQUFDO0lBRXBDLENBQUM7SUFFRCxLQUFLO1FBRUgsSUFBSSxDQUFDLElBQUksR0FBRyxFQUFFLENBQUM7UUFDZixJQUFJLENBQUMsS0FBSyxHQUFHLElBQUksVUFBVSxFQUFHLENBQUM7UUFDL0IsSUFBSSxDQUFDLEtBQUssR0FBRyxJQUFJLEdBQUcsRUFBRyxDQUFDO0lBRTFCLENBQUM7SUFFRCxLQUFLLENBQUMsTUFBTSxDQUFHLFVBQWdCLEVBQUUsT0FBZ0I7UUFFL0MsTUFBTSxJQUFJLEdBQUcsSUFBSSxDQUFDLFFBQVEsQ0FBRyxVQUFVLENBQUUsQ0FBQztRQUMxQyxNQUFNLElBQUksR0FBRyxNQUFNLElBQUksQ0FBQyxJQUFJLENBQUcsVUFBVSxFQUFFLE9BQU8sQ0FBRSxDQUFDO1FBRXJELElBQUksQ0FBQyxXQUFXLENBQUcsVUFBVSxFQUFFLElBQUksQ0FBRSxDQUFDO1FBRXRDLElBQUssQ0FBQyxJQUFJLElBQUksSUFBSSxFQUFHLENBQUM7WUFFcEIsSUFBSyxJQUFJLENBQUMsTUFBTSxFQUFHLEVBQUcsQ0FBQztnQkFFckIsSUFBSSxDQUFDLFNBQVMsQ0FBRyxVQUFVLEVBQUUsV0FBVyxDQUFDLEdBQUcsRUFBRSxJQUFJLENBQUUsQ0FBQztnQkFFckQsT0FBTyxDQUFDLFdBQVcsQ0FBQyxHQUFHLENBQUMsQ0FBQztZQUUzQixDQUFDO1lBRUQsSUFBSyxJQUFJLENBQUMsV0FBVyxFQUFHLEVBQUcsQ0FBQztnQkFFMUIsSUFBSSxDQUFDLFNBQVMsQ0FBRyxVQUFVLEVBQUUs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 \ No newline at end of file diff --git a/dist_ts/watcher_stats.d.ts b/dist_ts/watcher_stats.d.ts new file mode 100644 index 0000000..27ca6d4 --- /dev/null +++ b/dist_ts/watcher_stats.d.ts @@ -0,0 +1,17 @@ +import type { INO, Stats } from './types.js'; +declare class WatcherStats { + ino: INO; + size: number; + atimeMs: number; + mtimeMs: number; + ctimeMs: number; + birthtimeMs: number; + _isFile: boolean; + _isDirectory: boolean; + _isSymbolicLink: boolean; + constructor(stats: Stats); + isFile(): boolean; + isDirectory(): boolean; + isSymbolicLink(): boolean; +} +export default WatcherStats; diff --git a/dist_ts/watcher_stats.js b/dist_ts/watcher_stats.js new file mode 100644 index 0000000..4e09791 --- /dev/null +++ b/dist_ts/watcher_stats.js @@ -0,0 +1,30 @@ +/* IMPORT */ +/* MAIN */ +// An more memory-efficient representation of the useful subset of stats objects +class WatcherStats { + /* CONSTRUCTOR */ + constructor(stats) { + this.ino = (stats.ino <= Number.MAX_SAFE_INTEGER) ? Number(stats.ino) : stats.ino; + this.size = Number(stats.size); + this.atimeMs = Number(stats.atimeMs); + this.mtimeMs = Number(stats.mtimeMs); + this.ctimeMs = Number(stats.ctimeMs); + this.birthtimeMs = Number(stats.birthtimeMs); + this._isFile = stats.isFile(); + this._isDirectory = stats.isDirectory(); + this._isSymbolicLink = stats.isSymbolicLink(); + } + /* API */ + isFile() { + return this._isFile; + } + isDirectory() { + return this._isDirectory; + } + isSymbolicLink() { + return this._isSymbolicLink; + } +} +/* EXPORT */ +export default WatcherStats; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoid2F0Y2hlcl9zdGF0cy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3dhdGNoZXJfc3RhdHMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQ0EsWUFBWTtBQUlaLFVBQVU7QUFFVixnRkFBZ0Y7QUFFaEYsTUFBTSxZQUFZO0lBY2hCLGlCQUFpQjtJQUVqQixZQUFjLEtBQVk7UUFFeEIsSUFBSSxDQUFDLEdBQUcsR0FBRyxDQUFFLEtBQUssQ0FBQyxHQUFHLElBQUksTUFBTSxDQUFDLGdCQUFnQixDQUFFLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBRyxLQUFLLENBQUMsR0FBRyxDQUFFLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUM7UUFDdkYsSUFBSSxDQUFDLElBQUksR0FBRyxNQUFNLENBQUcsS0FBSyxDQUFDLElBQUksQ0FBRSxDQUFDO1FBQ2xDLElBQUksQ0FBQyxPQUFPLEdBQUcsTUFBTSxDQUFHLEtBQUssQ0FBQyxPQUFPLENBQUUsQ0FBQztRQUN4QyxJQUFJLENBQUMsT0FBTyxHQUFHLE1BQU0sQ0FBRyxLQUFLLENBQUMsT0FBTyxDQUFFLENBQUM7UUFDeEMsSUFBSSxDQUFDLE9BQU8sR0FBRyxNQUFNLENBQUcsS0FBSyxDQUFDLE9BQU8sQ0FBRSxDQUFDO1FBQ3hDLElBQUksQ0FBQyxXQUFXLEdBQUcsTUFBTSxDQUFHLEtBQUssQ0FBQyxXQUFXLENBQUUsQ0FBQztRQUNoRCxJQUFJLENBQUMsT0FBTyxHQUFHLEtBQUssQ0FBQyxNQUFNLEVBQUcsQ0FBQztRQUMvQixJQUFJLENBQUMsWUFBWSxHQUFHLEtBQUssQ0FBQyxXQUFXLEVBQUcsQ0FBQztRQUN6QyxJQUFJLENBQUMsZUFBZSxHQUFHLEtBQUssQ0FBQyxjQUFjLEVBQUcsQ0FBQztJQUVqRCxDQUFDO0lBRUQsU0FBUztJQUVULE1BQU07UUFFSixPQUFPLElBQUksQ0FBQyxPQUFPLENBQUM7SUFFdEIsQ0FBQztJQUVELFdBQVc7UUFFVCxPQUFPLElBQUksQ0FBQyxZQUFZLENBQUM7SUFFM0IsQ0FBQztJQUVELGNBQWM7UUFFWixPQUFPLElBQUksQ0FBQyxlQUFlLENBQUM7SUFFOUIsQ0FBQztDQUVGO0FBRUQsWUFBWTtBQUVaLGVBQWUsWUFBWSxDQUFDIn0= \ No newline at end of file diff --git a/license b/license new file mode 100644 index 0000000..969ded7 --- /dev/null +++ b/license @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2020-present Fabio Spampinato + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/npmextra.json b/npmextra.json new file mode 100644 index 0000000..84694ea --- /dev/null +++ b/npmextra.json @@ -0,0 +1,5 @@ +{ + "tsdoc": { + "legal": "The licenses of the original packages apply." + } +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100755 index 0000000..8b4daa2 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "@tempfix/watcher", + "repository": "github:fabiospampinato/watcher", + "description": "The file system watcher that strives for perfection, with no native dependencies and optional rename detection support.", + "version": "2.3.0", + "type": "module", + "main": "dist_ts/watcher.js", + "exports": "./dist_ts/watcher.js", + "types": "./dist_ts/watcher.d.ts", + "scripts": {}, + "keywords": [ + "fs", + "file", + "system", + "filesystem", + "watch", + "watcher" + ], + "dependencies": { + "stubborn-fs": "^1.2.5" + }, + "devDependencies": { + "@git.zone/tsbuild": "^2.1.72", + "@types/node": "^20.4.6" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..4d13f7c --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,921 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + stubborn-fs: + specifier: ^1.2.5 + version: 1.2.5 + +devDependencies: + '@git.zone/tsbuild': + specifier: ^2.1.72 + version: 2.1.72 + '@types/node': + specifier: ^20.4.6 + version: 20.11.8 + +packages: + + /@apiglobal/typedrequest-interfaces@2.0.1: + resolution: {integrity: sha512-Oi7pNU4vKo5UvcCJmqkH43Us237Ws/Pp/WDYnwnonRnTmIMd+6QjNfN/gXcPnP6tbamk8r8Xzcz9mgnSDM2ysw==} + dev: true + + /@git.zone/tsbuild@2.1.72: + resolution: {integrity: sha512-rVWM98chNjkt8pXdF5knGErZjM3GPnRXZYHVGECptxNvvhTol2DliM1OP8k3p3X5UOwEPV2sQVe//XzXs3BcUw==} + hasBin: true + dependencies: + '@push.rocks/early': 4.0.4 + '@push.rocks/smartcli': 4.0.8 + '@push.rocks/smartdelay': 3.0.5 + '@push.rocks/smartfile': 11.0.4 + '@push.rocks/smartlog': 3.0.3 + '@push.rocks/smartpath': 5.0.11 + '@push.rocks/smartpromise': 4.0.3 + typescript: 5.3.3 + dev: true + + /@isaacs/cliui@8.0.2: + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + dependencies: + string-width: 5.1.2 + string-width-cjs: /string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: /strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: /wrap-ansi@7.0.0 + dev: true + + /@pkgjs/parseargs@0.11.0: + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + requiresBuild: true + dev: true + optional: true + + /@push.rocks/consolecolor@2.0.1: + resolution: {integrity: sha512-iQx+sjxmvhRXjnv8aMiaZu125ZWfyZKSylCvKUNjVrdhkmP2uPy2VWsvyRBS4UiieYHTY/i+HpQiFv4QZV2/Fg==} + dependencies: + ansi-256-colors: 1.1.0 + dev: true + + /@push.rocks/early@4.0.4: + resolution: {integrity: sha512-ak6/vqZ1PlFV08fSFQ6UwiBrr+K6IsfieZWWzT7eex1Ls6GvWEi8wZ3REFDPJq/qckNLWSgEy0EsqzRtltkaCA==} + dependencies: + '@push.rocks/consolecolor': 2.0.1 + '@push.rocks/smartpromise': 4.0.3 + dev: true + + /@push.rocks/isounique@1.0.5: + resolution: {integrity: sha512-Z0BVqZZOCif1THTbIKWMgg0wxCzt9CyBtBBqQJiZ+jJ0KlQFrQHNHrPt81/LXe/L4x0cxWsn0bpL6W5DNSvNLw==} + dev: true + + /@push.rocks/lik@6.0.12: + resolution: {integrity: sha512-/vzlOZ26gCmXZz67LeM2hJ+aNM49Jxvf3FKxLMXHhJwffd3LcV96MYbMfKzKR/za/bh5Itf3a6UjLL5mmN6Pew==} + dependencies: + '@push.rocks/smartdelay': 3.0.5 + '@push.rocks/smartmatch': 2.0.0 + '@push.rocks/smartpromise': 4.0.3 + '@push.rocks/smartrx': 3.0.7 + '@push.rocks/smarttime': 4.0.6 + '@types/minimatch': 5.1.2 + '@types/symbol-tree': 3.2.5 + symbol-tree: 3.2.4 + dev: true + + /@push.rocks/smartcli@4.0.8: + resolution: {integrity: sha512-B4F3nqq7ko8tev1wxGdFnh/zSDDP8Q9LpEOb3wTf0jayyhYetFQ7n6zi4J9fhXYBKPkJSyQEBoOfRmgJyeLHkA==} + dependencies: + '@push.rocks/lik': 6.0.12 + '@push.rocks/smartlog': 3.0.3 + '@push.rocks/smartparam': 1.1.10 + '@push.rocks/smartpromise': 4.0.3 + '@push.rocks/smartrx': 3.0.7 + yargs-parser: 21.1.1 + dev: true + + /@push.rocks/smartdelay@3.0.5: + resolution: {integrity: sha512-mUuI7kj2f7ztjpic96FvRIlf2RsKBa5arw81AHNsndbxO6asRcxuWL8dTVxouEIK8YsBUlj0AsrCkHhMbLQdHw==} + dependencies: + '@push.rocks/smartpromise': 4.0.3 + dev: true + + /@push.rocks/smartenv@5.0.12: + resolution: {integrity: sha512-tDEFwywzq0FNzRYc9qY2dRl2pgQuZG0G2/yml2RLWZWSW+Fn1EHshnKOGHz8o77W7zvu4hTgQQX42r/JY5XHTg==} + dependencies: + '@push.rocks/smartpromise': 4.0.3 + dev: true + + /@push.rocks/smartfile-interfaces@1.0.7: + resolution: {integrity: sha512-MeOl/200UOvSO4Pgq/DVFiBVZpL9gjOBQM+4XYNjSxda8c6VBvchHAntaFLQUlO8U1ckNaP9i+nMO4O4/0ymyw==} + dev: true + + /@push.rocks/smartfile@11.0.4: + resolution: {integrity: sha512-NXAyqYE5zNUJ9Mu/t2oWUKu21CRUI4Dvlm56rKBSczCq5xeC7EwmamTzL3Nyn6Tmu1jBpYktYL4zIx17JJOB7w==} + dependencies: + '@push.rocks/lik': 6.0.12 + '@push.rocks/smartdelay': 3.0.5 + '@push.rocks/smartfile-interfaces': 1.0.7 + '@push.rocks/smarthash': 3.0.4 + '@push.rocks/smartjson': 5.0.10 + '@push.rocks/smartmime': 1.0.6 + '@push.rocks/smartpath': 5.0.11 + '@push.rocks/smartpromise': 4.0.3 + '@push.rocks/smartrequest': 2.0.21 + '@push.rocks/smartstream': 3.0.30 + '@types/fs-extra': 11.0.4 + '@types/glob': 8.1.0 + '@types/js-yaml': 4.0.9 + fs-extra: 11.2.0 + glob: 10.3.10 + js-yaml: 4.1.0 + dev: true + + /@push.rocks/smarthash@3.0.4: + resolution: {integrity: sha512-HJ/fSx41jm0CvSaqMLa6b2nuNK5rHAqAeAq3dAB7Sq9BCPm2M0J5ZVDTzEAH8pS91XYniUiwuE0jwPERNn9hmw==} + dependencies: + '@push.rocks/smartjson': 5.0.10 + '@push.rocks/smartpromise': 4.0.3 + '@types/through2': 2.0.41 + through2: 4.0.2 + dev: true + + /@push.rocks/smartjson@5.0.10: + resolution: {integrity: sha512-yuntSMGZ+XNHMrbS9RxotaD+eOgoNTcuDoWsttis+N3Mkc9DIam0pt/ER4NU8TgfMmhT/hKwQH+3DJceDzntoA==} + dependencies: + '@push.rocks/smartstring': 4.0.13 + '@types/buffer-json': 2.0.3 + buffer-json: 2.0.0 + fast-json-stable-stringify: 2.1.0 + lodash.clonedeep: 4.5.0 + dev: true + + /@push.rocks/smartlog-interfaces@3.0.0: + resolution: {integrity: sha512-dfRqiSolGQwaF9gWmkixWOoXZxcWBjK3u6A1CpcfhCbVr2VSUMIrZ5t74/DgdfedsTrhDqoD0NGezsMXF2pFHQ==} + dependencies: + '@apiglobal/typedrequest-interfaces': 2.0.1 + dev: true + + /@push.rocks/smartlog@3.0.3: + resolution: {integrity: sha512-E4UUSdbrf0TdSqI7LrUa3jgYQGKT6+ybSHuRcopFDt0W2/tBpY+/vPyAApJIa8iGFKJoi3oSTgYJbK90SwQwKg==} + dependencies: + '@push.rocks/isounique': 1.0.5 + '@push.rocks/smartlog-interfaces': 3.0.0 + dev: true + + /@push.rocks/smartmatch@2.0.0: + resolution: {integrity: sha512-MBzP++1yNIBeox71X6VxpIgZ8m4bXnJpZJ4nWVH6IWpmO38MXTu4X0QF8tQnyT4LFcwvc9iiWaD15cstHa7Mmw==} + dependencies: + matcher: 5.0.0 + dev: true + + /@push.rocks/smartmime@1.0.6: + resolution: {integrity: sha512-PHd+I4UcsnOATNg8wjDsSAmmJ4CwQFrQCNzd0HSJMs4ZpiK3Ya91almd6GLpDPU370U4HFh4FaPF4eEAI6vkJQ==} + dependencies: + '@types/mime-types': 2.1.4 + mime-types: 2.1.35 + dev: true + + /@push.rocks/smartparam@1.1.10: + resolution: {integrity: sha512-2WDAUtc7GH+E0QszsiuXRdLPnJ/edlS2zPtFgfNpA0LJ8tJ5J9lyx6zhM39k4rKzKtK7bnjWHDb2tHE9zaOBYw==} + deprecated: deprecated in favour of @push.rocks/smartobject + dependencies: + '@push.rocks/smartpromise': 4.0.3 + minimatch: 9.0.3 + dev: true + + /@push.rocks/smartpath@5.0.11: + resolution: {integrity: sha512-dqdd7KTby0AdaWYC9gVoHDTUIixFhEvo+mmdaTdNshZsfHNkm/EDV25dA+9gJ8/yoyuCYmrwmByNYy9a+xFUeQ==} + dev: true + + /@push.rocks/smartpromise@4.0.3: + resolution: {integrity: sha512-z3lIso4/6KK3c6NFTVGZ7AOBsGURf8ha3qQtX/OxjZFk5dqS//8PLd0XqghVdIaUlRGmJ7Sfds/efZERWn1tAg==} + dev: true + + /@push.rocks/smartrequest@2.0.21: + resolution: {integrity: sha512-btk9GbiMNxNcEgJEqTq9qMFJ/6ua6oG4q49v+8ujKAXU50vFn1WQ/H0VAyeu9LMa5GCcRwUhNNDdwpLVGVbrBg==} + dependencies: + '@push.rocks/smartpromise': 4.0.3 + '@push.rocks/smarturl': 3.0.7 + agentkeepalive: 4.5.0 + form-data: 4.0.0 + dev: true + + /@push.rocks/smartrx@3.0.7: + resolution: {integrity: sha512-qCWy0s3RLAgGSnaw/Gu0BNaJ59CsI6RK5OJDCCqxc7P2X/S755vuLtnAR5/0dEjdhCHXHX9ytPZx+o9g/CNiyA==} + dependencies: + '@push.rocks/smartpromise': 4.0.3 + rxjs: 7.8.1 + dev: true + + /@push.rocks/smartstream@3.0.30: + resolution: {integrity: sha512-+izraXkILJJIy99PzP2LYahaW+g/35bTi/UxD7FeuOYbTaigode6Q3swvs0nrK6yu+A9x6RfoWV4JAJjd3Y87g==} + dependencies: + '@push.rocks/lik': 6.0.12 + '@push.rocks/smartpromise': 4.0.3 + '@push.rocks/smartrx': 3.0.7 + dev: true + + /@push.rocks/smartstring@4.0.13: + resolution: {integrity: sha512-iEAch6fYC+VijBYWFfRif5Wj5KxdUgC2Xnn0NNgDFrBmI14HsECcPbZ0YdESawRVD27pLYYZJCCbu/M/Llo1kg==} + dependencies: + '@push.rocks/isounique': 1.0.5 + '@push.rocks/smartenv': 5.0.12 + '@types/randomatic': 3.1.5 + buffer: 6.0.3 + crypto-random-string: 5.0.0 + js-base64: 3.7.6 + normalize-newline: 4.1.0 + randomatic: 3.1.1 + strip-indent: 4.0.0 + url: 0.11.3 + dev: true + + /@push.rocks/smarttime@4.0.6: + resolution: {integrity: sha512-1whOow0YJw/TbN758TedRRxApoZbsvyxCVpoGjXh7DE/fEEgs7RCr4vVF5jYpyXNQuNMLpKJcTsSfyQ6RvH4Aw==} + dependencies: + '@push.rocks/lik': 6.0.12 + '@push.rocks/smartdelay': 3.0.5 + '@push.rocks/smartpromise': 4.0.3 + croner: 7.0.5 + dayjs: 1.11.10 + is-nan: 1.3.2 + pretty-ms: 8.0.0 + dev: true + + /@push.rocks/smarturl@3.0.7: + resolution: {integrity: sha512-nx4EWjQD9JeO7QVbOsxd1PFeDQYoSQOOOYCZ+r7QWXHLJG52iYzgvJDCQyX6p705HDkYMJWozW2ZzhR22qLKbw==} + dev: true + + /@types/buffer-json@2.0.3: + resolution: {integrity: sha512-ItD4UfF3Q5jA+PEV6ZUWEHvlWaXJbd0rpuBKOIrEebM053FHaJddKsgUf0vy7nLSTs44nqFj3Mh8J3TiT0xv4g==} + dev: true + + /@types/fs-extra@11.0.4: + resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} + dependencies: + '@types/jsonfile': 6.1.4 + '@types/node': 20.11.8 + dev: true + + /@types/glob@8.1.0: + resolution: {integrity: sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==} + dependencies: + '@types/minimatch': 5.1.2 + '@types/node': 20.11.8 + dev: true + + /@types/js-yaml@4.0.9: + resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + dev: true + + /@types/jsonfile@6.1.4: + resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} + dependencies: + '@types/node': 20.11.8 + dev: true + + /@types/mime-types@2.1.4: + resolution: {integrity: sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==} + dev: true + + /@types/minimatch@5.1.2: + resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} + dev: true + + /@types/node@20.11.8: + resolution: {integrity: sha512-i7omyekpPTNdv4Jb/Rgqg0RU8YqLcNsI12quKSDkRXNfx7Wxdm6HhK1awT3xTgEkgxPn3bvnSpiEAc7a7Lpyow==} + dependencies: + undici-types: 5.26.5 + dev: true + + /@types/randomatic@3.1.5: + resolution: {integrity: sha512-VCwCTw6qh1pRRw+5rNTAwqPmf6A+hdrkdM7dBpZVmhl7g+em3ONXlYK/bWPVKqVGMWgP0d1bog8Vc/X6zRwRRQ==} + dev: true + + /@types/symbol-tree@3.2.5: + resolution: {integrity: sha512-zXnnyENt1TYQcS21MkPaJCVjfcPq7p7yc5mo5JACuumXp6sly5jnlS0IokHd+xmmuCbx6V7JqkMBpswR+nZAcw==} + dev: true + + /@types/through2@2.0.41: + resolution: {integrity: sha512-ryQ0tidWkb1O1JuYvWKyMLYEtOWDqF5mHerJzKz/gQpoAaJq2l/dsMPBF0B5BNVT34rbARYJ5/tsZwLfUi2kwQ==} + dependencies: + '@types/node': 20.11.8 + dev: true + + /agentkeepalive@4.5.0: + resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==} + engines: {node: '>= 8.0.0'} + dependencies: + humanize-ms: 1.2.1 + dev: true + + /ansi-256-colors@1.1.0: + resolution: {integrity: sha1-kQ3lDvzHwJ49gvL4er1rcAwYgYo=} + engines: {node: '>=0.10.0'} + dev: true + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: true + + /ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + dev: true + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: true + + /ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + dev: true + + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: true + + /asynckit@0.4.0: + resolution: {integrity: sha1-x57Zf380y48robyXkLzDZkdLS3k=} + dev: true + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: true + + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: true + + /buffer-json@2.0.0: + resolution: {integrity: sha512-+jjPFVqyfF1esi9fvfUs3NqM0pH1ziZ36VP4hmA/y/Ssfo/5w5xHKfTw9BwQjoJ1w/oVtpLomqwUHKdefGyuHw==} + dev: true + + /buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: true + + /call-bind@1.0.5: + resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} + dependencies: + function-bind: 1.1.2 + get-intrinsic: 1.2.2 + set-function-length: 1.2.0 + dev: true + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: true + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true + + /combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + dev: true + + /croner@7.0.5: + resolution: {integrity: sha512-15HLCD7iXnMe5km54yc4LN5BH+Cg9uCQvbkJ0acHxFffE29w3Uvgb9s/l310UCVUgMwGSBNw9BAHsEb5uMgj1g==} + engines: {node: '>=6.0'} + dev: true + + /cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + dev: true + + /crypto-random-string@5.0.0: + resolution: {integrity: sha512-KWjTXWwxFd6a94m5CdRGW/t82Tr8DoBc9dNnPCAbFI1EBweN6v1tv8y4Y1m7ndkp/nkIBRxUxAzpaBnR2k3bcQ==} + engines: {node: '>=14.16'} + dependencies: + type-fest: 2.19.0 + dev: true + + /dayjs@1.11.10: + resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==} + dev: true + + /define-data-property@1.1.1: + resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.2 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + dev: true + + /define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + has-property-descriptors: 1.0.1 + object-keys: 1.1.1 + dev: true + + /delayed-stream@1.0.0: + resolution: {integrity: sha1-3zrhmayt+31ECqrgsp4icrJOxhk=} + engines: {node: '>=0.4.0'} + dev: true + + /eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: true + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: true + + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: true + + /escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + dev: true + + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: true + + /foreground-child@3.1.1: + resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} + engines: {node: '>=14'} + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + dev: true + + /form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: true + + /fs-extra@11.2.0: + resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} + engines: {node: '>=14.14'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + dev: true + + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + dev: true + + /get-intrinsic@1.2.2: + resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} + dependencies: + function-bind: 1.1.2 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.0 + dev: true + + /glob@10.3.10: + resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + dependencies: + foreground-child: 3.1.1 + jackspeak: 2.3.6 + minimatch: 9.0.3 + minipass: 7.0.4 + path-scurry: 1.10.1 + dev: true + + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.2 + dev: true + + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + dev: true + + /has-property-descriptors@1.0.1: + resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==} + dependencies: + get-intrinsic: 1.2.2 + dev: true + + /has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + dev: true + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: true + + /hasown@2.0.0: + resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: true + + /humanize-ms@1.2.1: + resolution: {integrity: sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=} + dependencies: + ms: 2.1.3 + dev: true + + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: true + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: true + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: true + + /is-nan@1.3.2: + resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + dev: true + + /is-number@4.0.0: + resolution: {integrity: sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==} + engines: {node: '>=0.10.0'} + dev: true + + /isexe@2.0.0: + resolution: {integrity: sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=} + dev: true + + /jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} + engines: {node: '>=14'} + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + dev: true + + /js-base64@3.7.6: + resolution: {integrity: sha512-NPrWuHFxFUknr1KqJRDgUQPexQF0uIJWjeT+2KjEePhitQxQEx5EJBG1lVn5/hc8aLycTpXrDOgPQ6Zq+EDiTA==} + dev: true + + /js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: true + + /jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + dev: true + + /kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + dev: true + + /lodash.clonedeep@4.5.0: + resolution: {integrity: sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=} + dev: true + + /lru-cache@10.2.0: + resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==} + engines: {node: 14 || >=16.14} + dev: true + + /matcher@5.0.0: + resolution: {integrity: sha512-s2EMBOWtXFc8dgqvoAzKJXxNHibcdJMV0gwqKUaw9E2JBJuGUK7DrNKrA6g/i+v72TT16+6sVm5mS3thaMLQUw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + escape-string-regexp: 5.0.0 + dev: true + + /math-random@1.0.4: + resolution: {integrity: sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==} + dev: true + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: true + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: true + + /min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + dev: true + + /minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /minipass@7.0.4: + resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} + engines: {node: '>=16 || 14 >=14.17'} + dev: true + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: true + + /normalize-newline@4.1.0: + resolution: {integrity: sha512-ff4jKqMI8Xl50/4Mms/9jPobzAV/UK+kXG2XJ/7AqOmxIx8mqfqTIHYxuAnEgJ2AQeBbLnlbmZ5+38Y9A0w/YA==} + engines: {node: '>=12'} + dependencies: + replace-buffer: 1.2.1 + dev: true + + /object-inspect@1.13.1: + resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} + dev: true + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + dev: true + + /parse-ms@3.0.0: + resolution: {integrity: sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw==} + engines: {node: '>=12'} + dev: true + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + dev: true + + /path-scurry@1.10.1: + resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + lru-cache: 10.2.0 + minipass: 7.0.4 + dev: true + + /pretty-ms@8.0.0: + resolution: {integrity: sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q==} + engines: {node: '>=14.16'} + dependencies: + parse-ms: 3.0.0 + dev: true + + /punycode@1.4.1: + resolution: {integrity: sha1-wNWmOycYgArY4esPpSachN1BhF4=} + dev: true + + /qs@6.11.2: + resolution: {integrity: sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.4 + dev: true + + /randomatic@3.1.1: + resolution: {integrity: sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==} + engines: {node: '>= 0.10.0'} + dependencies: + is-number: 4.0.0 + kind-of: 6.0.3 + math-random: 1.0.4 + dev: true + + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: true + + /replace-buffer@1.2.1: + resolution: {integrity: sha512-ly3OKwKu+3T55DjP5PjIMzxgz9lFx6dQnBmAIxryZyRKl8f22juy12ShOyuq8WrQE5UlFOseZgQZDua0iF9DHw==} + engines: {node: '>=4'} + dev: true + + /rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + dependencies: + tslib: 2.6.2 + dev: true + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: true + + /set-function-length@1.2.0: + resolution: {integrity: sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + function-bind: 1.1.2 + get-intrinsic: 1.2.2 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + dev: true + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + dev: true + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + dev: true + + /side-channel@1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + object-inspect: 1.13.1 + dev: true + + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + dev: true + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: true + + /string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + dev: true + + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: true + + /strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 + dev: true + + /strip-indent@4.0.0: + resolution: {integrity: sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==} + engines: {node: '>=12'} + dependencies: + min-indent: 1.0.1 + dev: true + + /stubborn-fs@1.2.5: + resolution: {integrity: sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g==} + dev: false + + /symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + dev: true + + /through2@4.0.2: + resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} + dependencies: + readable-stream: 3.6.2 + dev: true + + /tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + dev: true + + /type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + dev: true + + /typescript@5.3.3: + resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} + engines: {node: '>=14.17'} + hasBin: true + dev: true + + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + dev: true + + /universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + dev: true + + /url@0.11.3: + resolution: {integrity: sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==} + dependencies: + punycode: 1.4.1 + qs: 6.11.2 + dev: true + + /util-deprecate@1.0.2: + resolution: {integrity: sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=} + dev: true + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: true + + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + + /wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + dev: true + + /yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + dev: true diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..a055427 --- /dev/null +++ b/readme.md @@ -0,0 +1,242 @@ +# Watcher + +The file system watcher that strives for perfection, with no native dependencies and optional rename detection support. + +## Features + +- **Reliable**: This library aims to handle all issues that may possibly arise when dealing with the file system, including some the most popular alternatives don't handle, like EMFILE errors. +- **Rename detection**: This library can optionally detect when files and directories are renamed, which allows you to provide a better experience to your users in some cases. +- **Performant**: Native recursive watching is used when available (macOS and Windows), and it's efficiently manually performed otherwise. +- **No native dependencies**: Native dependencies can be painful to work with, this library uses 0 of them. +- **No bloat**: Many alternative watchers ship with potentially useless and expensive features, like support for globbing, this library aims to be much leaner while still exposing the right abstractions that allow you to use globbing if you want to. +- **TypeScript-ready**: This library is written in TypeScript, so types aren't an afterthought but come with the library. + +## Comparison + +You are probably currently using one of the following alternatives for file system watching, here's how they compare against Watcher: + +- `fs.watch`: Node's built-in `fs.watch` function is essentially garbage and you never want to use it directly. + - Cons: + - Recursive watching is not supported under Linux, so if you need to support Linux at all you are out of luck already. + - Even if you only need to support macOS or Windows, where native recursive watching is provided, the events provided by `fs.watch` are completely useless as they tell you nothing about what actually happened in the file system, so you'll have to poll the file system on your own anyway. + - There are many things that `fs.watch` doesn't take care of, for example watching non-existent paths is just not supported and EMFILE errors are not handled. +- [`chokidar`](https://github.com/paulmillr/chokidar): this is the most popular file system watcher available, while it may be good enough in some cases it's not perfect. + - Cons: + - It requires a native dependency for efficient recursive watching under macOS, and native dependencies can be a pain to work with. + - It doesn't watch recursively efficiently under Windows, Watcher on the other hand is built upon Node's native recursive watching capabilities for Windows. + - It can't detect renames. + - If you don't need features like globbing then chokidar will bloat your app bundles unnecessarely. + - EMFILE errors are not handled properly, so if you are watching enough files chokidar will eventually just give up on them. + - It's not very actively maintened, Watcher on the other hand strives for having 0 bugs, if you can find some we'll fix them ASAP. + - Pros: + - It supports handling symlinks. + - It has some built-in support for handling temporary files written to disk while perfoming an atomic write, although ignoring them in Watcher is pretty trivial too, you can ignore them via the `ignore` option. + - It can more reliably watch network attached paths, although that will lead to performance issues when watching ~lots of files. + - It's more battle tested, although Watcher has a more comprehensive test suite and is used in production too (for example in [Notable](https://github.com/notable/notable), which was using `chokidar` before). +- [`node-watch`](https://github.com/yuanchuan/node-watch): in some ways this library is similar to Watcher, but much less mature. + - Cons: + - No initial events can be emitted when starting watching. + - Only the "update" or "remove" events are emitted, which tell you nothing about whether each event refers to a file or a directory, or whether a file got added or modified. + - "add" and "unlink" events are not provided in some cases, like for files inside an added/deleted folder. + - Watching non-existent paths is not supported. + - It can't detect renames. +- [`nsfw`](https://github.com/Axosoft/nsfw): this is a lesser known but pretty good watcher, although it comes with some major drawbacks. + - Cons: + - It's based on native dependencies, which can be a pain to work with, especially considering that prebuild binaries are not provided so you have to build them yourself. + - It's not very customizable, so for example instructing the watcher to ignore some paths is not possible. + - Everything being native makes it more difficult to contribute a PR or a test to it. + - It's not very actively maintained. + - Pros: + - It adds next to 0 overhead to the rest of your app, as the watching is performed in a separate process and events are emitted in batches. +- "`perfection`": if there was a "perfect" file system watcher, it would compare like this against Watcher (i.e. this is pretty much what's currently missing in Watcher): + - Pros: + - It would support symlinks, Watcher doesn't handle them just yet. + - It would watch all parent directories of the watched roots, for unlink detection when those parents get unlinked, Watcher currently also watches only up-to 1 level parents, which is more than what most other watchers do though. + - It would provide some simple and efficient APIs for adding and removing paths to watch from/to a watcher instance, Watcher currently only has some internal APIs that could be used for that but they are not production-ready yet, although closing a watcher and making a new one with the updated paths to watch works well enough in most cases. + - It would add next to 0 overhead to the rest of your app, currenly Watcher adds some overhead to your app, but if that's significant for your use cases we would consider that to be a bug. You could potentially already spawn a separate process and do the file system watching there yourself too. + - Potentially there are some more edge cases that should be handled too, if you know about them or can find any bug in Watcher just open an issue and we'll fix it ASAP. + +## Install + +```sh +npm install --save watcher +``` + +## Options + +The following options are provided, you can use them to customize watching to your needs: + +- `debounce`: amount of milliseconds to debounce event emission for. + - by default this is set to `300`. + - the higher this is the more duplicate events will be ignored automatically. +- `depth`: maximum depth to watch files at. + - by default this is set to `20`. + - this is useful for avoiding watching directories that are absurdly deep, that would probably waste resources. +- `limit`: maximum number of paths to prod. + - by default this is set to `10_000_000`. + - this is useful as a safe guard in cases where for example the user decided to watch `/`, perhaps by mistake. +- `ignore`: optional function (or regex) that if returns `true` for a path it will cause that path and all its descendants to not be watched at all. + - by default this is not set, so all paths are watched. + - setting an `ignore` function can be very important for performance, you should probably ignore folders like `.git` and temporary files like those used when writing atomically to disk. + - if you need globbing you'll just have to match the path passed to `ignore` against a glob with a globbing library of your choosing. +- `ignoreInitial`: whether events for the initial scan should be ignored or not. + - by default this is set to `false`, so initial events are emitted. +- `native`: whether to use the native recursive watcher if available and needed. + - by default this is set to `true`. + - the native recursive watcher is only available under macOS and Windows. + - when the native recursive watcher is used the `depth` option is ignored. + - setting it to `false` can have a positive performance impact if you want to watch recursively a potentially very deep directory with a low `depth` value. +- `persistent`: whether to keep the Node process running as long as the watcher is not closed. + - by default this is set to `false`. +- `pollingInterval`: polling is used as a last resort measure when watching non-existent paths inside non-existent directories, this controls how often polling is performed, in milliseconds. + - by default this is set to `3000`. + - you can set it to a lower value to make the app detect events much more quickly, but don't set it too low if you are watching many paths that require polling as polling is expensive. +- `pollingTimeout`: sometimes polling will fail, for example if there are too many file descriptors currently open, usually eventually polling will succeed after a few tries though, this controls the amount of milliseconds the library should keep retrying for. + - by default this is set to `20000`. +- `recursive`: whether to watch recursively or not. + - by default this is set to `false`. + - this is supported under all OS'. + - this is implemented natively by Node itself under macOS and Windows. +- `renameDetection`: whether the library should attempt to detect renames and emit `rename`/`renameDir` events. + - by default this is set to `false`. + - rename detection may cause a delayed event emission, because the library may have to wait some more time for it. + - if disabled, the raw underlying `add`/`addDir` and `unlink`/`unlinkDir` events will be emitted instead after a rename. + - if enabled, the library will check if each pair of `add`/`unlink` or `addDir`/`unlinkDir` events are actually `rename` or `renameDir` events respectively, so it will wait for both of those events to be emitted. + - rename detection is fairly reliable, but it is fundamentally dependent on how long the file system takes to emit the underlying raw events, if it takes longer than the set rename timeout the app won't detect the rename and will instead emit the underlying raw events. +- `renameTimeout`: amount of milliseconds to wait for a potential `rename`/`renameDir` event to be detected. + - by default this is set to `1250`. + - the higher this value is the more reliably renames will be detected, but don't set this too high, or the emission of some events could be delayed by that amount. + - the higher this value is the longer the library will take to emit `add`/`addDir`/`unlink`/`unlinkDir` events. + +## Usage + +Watcher returns an `EventEmitter` instance, so all the methods inherited from that are supported, and the API is largely event-driven. + +The following events are emitted: + +- Watcher events: + - `error`: Emitted whenever an error occurs. + - `ready`: Emitted after the Watcher has finished instantiating itself. No events are emitted before this events, expect potentially for the `error` event. + - `close`: Emitted when the watcher gets explicitly closed and all its watching operations are stopped. No further events will be emitted after this event. + - `all`: Emitted right before a file system event is about to get emitted. +- File system events: + - `add`: Emitted when a new file is added. + - `addDir`: Emitted when a new directory is added. + - `change`: Emitted when an existing file gets changed, maybe its content changed, maybe its metadata changed. + - `rename`: Emitted when a file gets renamed. This is only emitted when `renameDetection` is enabled. + - `renameDir`: Emitted when a directory gets renamed. This is only emitted when `renameDetection` is enabled. + - `unlink`: Emitted when a file gets removed from the watched tree. + - `unlinkDir`: Emitted when a directory gets removed from the watched tree. + +Basically it you have used [`chokidar`](https://github.com/paulmillr/chokidar) in the past Watcher emits pretty much the same exact events, except that it can also emit `rename`/`renameDir` events, it doesn't provide `stats` objects but only paths, and in general it exposes a similar API surface, so switching from (or to) `chokidar` should be easy. + +The following interface is provided: + +```ts +type Roots = string[] | string; + +type TargetEvent = 'add' | 'addDir' | 'change' | 'rename' | 'renameDir' | 'unlink' | 'unlinkDir'; +type WatcherEvent = 'all' | 'close' | 'error' | 'ready'; +type Event = TargetEvent | WatcherEvent; + +type Options = { + debounce?: number, + depth?: number, + ignore?: (( targetPath: Path ) => boolean) | RegExp, + ignoreInitial?: boolean, + native?: boolean, + persistent?: boolean, + pollingInterval?: number, + pollingTimeout?: number, + recursive?: boolean, + renameDetection?: boolean, + renameTimeout?: number +}; + +class Watcher { + constructor ( roots: Roots, options?: Options, handler?: Handler ): this; + on ( event: Event, handler: Function ): this; + close (): void; +} +``` + +You would use the library like this: + +```ts +import Watcher from 'watcher'; + +// Watching a single path +const watcher = new Watcher ( '/foo/bar' ); + +// Watching multiple paths +const watcher = new Watcher ( ['/foo/bar', '/baz/qux'] ); + +// Passing some options +const watcher = new Watcher ( '/foo/bar', { renameDetection: true } ); + +// Passing an "all" handler directly +const watcher = new Watcher ( '/foo/bar', {}, ( event, targetPath, targetPathNext ) => {} ); + +// Attaching the "all" handler manually +const watcher = new Watcher ( '/foo/bar' ); +watcher.on ( 'all', ( event, targetPath, targetPathNext ) => { // This is what the library does internally when you pass it a handler directly + console.log ( event ); // => could be any target event: 'add', 'addDir', 'change', 'rename', 'renameDir', 'unlink' or 'unlinkDir' + console.log ( targetPath ); // => the file system path where the event took place, this is always provided + console.log ( targetPathNext ); // => the file system path "targetPath" got renamed to, this is only provided on 'rename'/'renameDir' events +}); + +// Listening to individual events manually +const watcher = new Watcher ( '/foo/bar' ); + +watcher.on ( 'error', error => { + console.log ( error instanceof Error ); // => true, "Error" instances are always provided on "error" +}); +watcher.on ( 'ready', () => { + // The app just finished instantiation and may soon emit some events +}); +watcher.on ( 'close', () => { + // The app just stopped watching and will not emit any further events +}); +watcher.on ( 'all', ( event, targetPath, targetPathNext ) => { + console.log ( event ); // => could be any target event: 'add', 'addDir', 'change', 'rename', 'renameDir', 'unlink' or 'unlinkDir' + console.log ( targetPath ); // => the file system path where the event took place, this is always provided + console.log ( targetPathNext ); // => the file system path "targetPath" got renamed to, this is only provided on 'rename'/'renameDir' events +}); +watcher.on ( 'add', filePath => { + console.log ( filePath ); // "filePath" just got created, or discovered by the watcher if this is an initial event +}); +watcher.on ( 'addDir', directoryPath => { + console.log ( directoryPath ); // "directoryPath" just got created, or discovered by the watcher if this is an initial event +}); +watcher.on ( 'change', filePath => { + console.log ( filePath ); // "filePath" just got modified +}); +watcher.on ( 'rename', ( filePath, filePathNext ) => { + console.log ( filePath, filePathNext ); // "filePath" got renamed to "filePathNext" +}); +watcher.on ( 'renameDir', ( directoryPath, directoryPathNext ) => { + console.log ( directoryPath, directoryPathNext ); // "directoryPath" got renamed to "directoryPathNext" +}); +watcher.on ( 'unlink', filePath => { + console.log ( filePath ); // "filePath" got deleted, or at least moved outside the watched tree +}); +watcher.on ( 'unlinkDir', directoryPath => { + console.log ( directoryPath ); // "directoryPath" got deleted, or at least moved outside the watched tree +}); + +// Closing the watcher once you are done with it +watcher.close (); + +// Updating watched roots by closing a watcher and opening an updated one +watcher.close (); +watcher = new Watcher ( /* Updated options... */ ); +``` + +## Thanks + +- [`chokidar`](https://github.com/paulmillr/chokidar): for providing me a largely good-enough file system watcher for a long time. +- [`node-watch`](https://github.com/yuanchuan/node-watch): for providing a good base from with to make Watcher, and providing some good ideas for how to write good tests for it. + +## License + +MIT © Fabio Spampinato diff --git a/test/hooks.js b/test/hooks.js new file mode 100644 index 0000000..3f08cec --- /dev/null +++ b/test/hooks.js @@ -0,0 +1,167 @@ + +/* IMPORT */ + +import fs from 'node:fs'; +import {setTimeout as delay} from 'node:timers/promises'; +import Watcher from '../dist/watcher.js'; +import Tree from './tree.js'; + +/* HELPERS */ + +let TREES = []; + +/* MAIN */ + +//TODO: Use actual hooks, once those get fixed in "fava" + +const before = async () => { + + if ( fs.existsSync ( Tree.ROOT ) ) { + fs.rmdirSync ( Tree.ROOT, { recursive: true } ); + } + + TREES = await Promise.all ( Array ( 190 ).fill ().map ( async ( _, i ) => { + const tree = new Tree ( i ); + await tree.build (); + return tree; + })); + + await delay ( 5000 ); // Giving the filesystem enough time to chill + +}; + +const beforeEach = t => { + + const isEqual = ( a, b ) => JSON.stringify ( a ) === JSON.stringify ( b ); + const prettyprint = value => JSON.stringify ( value, undefined, 2 ); + + t.context.normalizePaths = paths => { + return paths.map ( path => { + return Array.isArray ( path ) ? t.context.normalizePaths ( path ) : t.context.tree.path ( path ); + }); + }; + + t.context.hasWatchObjects = ( pollersNr, subwatchersNr, watchersNr ) => { + t.is ( t.context.watcher._pollers.size, pollersNr, 'pollers number' ); + t.is ( t.context.watcher._subwatchers.size, subwatchersNr, 'subwatchers number' ); + t.is ( Object.keys ( t.context.watcher._watchers ).map ( key => t.context.watcher._watchers[key] ).flat ().length, watchersNr, 'watchers number' ); + }; + + t.context.deepEqualUnordered = ( a, b ) => { + t.is ( a.length, b.length ); + t.true ( a.every ( item => { + const index = b.findIndex ( itemOther => isEqual ( item, itemOther ) ); + if ( index === -1 ) return false; + b.splice ( index, 1 ); + return true; + }), prettyprint ( [a, b] ) ); + }; + + t.context.deepEqualUnorderedTuples = ( [a1, b1], [a2, b2] ) => { + t.is ( a1.length, b1.length ); + t.is ( a1.length, a2.length ); + t.is ( b1.length, b2.length ); + t.true ( a1.every ( ( item, itemIndex ) => { + for ( let i = 0, l = a2.length; i < l; i++ ) { + if ( !isEqual ( item, a2[i] ) ) continue; + if ( !isEqual ( b1[itemIndex], b2[i] ) ) continue; + a1.splice ( itemIndex, 1 ); + b1.splice ( itemIndex, 1 ); + a2.splice ( i, 1 ); + b2.splice ( i, 1 ); + return true; + } + return false; + }), prettyprint ( [[a1, b1], [a2, b2]] ) ); + }; + + t.context.deepEqualChanges = changes => { + t.deepEqual ( t.context.changes, t.context.normalizePaths ( changes ) ); + }; + + t.context.deepEqualUnorderedChanges = changes => { + t.context.deepEqualUnordered ( t.context.changes, t.context.normalizePaths ( changes ) ); + }; + + t.context.deepEqualResults = ( events, changes ) => { + t.deepEqual ( t.context.events, events ); + t.deepEqual ( t.context.changes, t.context.normalizePaths ( changes ) ); + t.context.watchReset (); + }; + + t.context.deepEqualUnorderedResults = ( events, changes ) => { + t.context.deepEqualUnorderedTuples ( [t.context.events, t.context.changes], [events, t.context.normalizePaths ( changes )] ); + t.context.watchReset (); + }; + + t.context.watch = ( target, options = {}, handler = () => {}, filterer = () => true ) => { + const targets = t.context.normalizePaths ( Array.isArray ( target ) ? target : [target] ); + t.context.events = []; + t.context.changes = []; + t.context.watcher = new Watcher ( targets, options, ( event, targetPath, targetPathNext ) => { + if ( !filterer ( event, targetPath ) ) return; + const change = targetPathNext ? [targetPath, targetPathNext] : targetPath; + t.context.events.push ( event ); + t.context.changes.push ( change ); + handler ( event, change ); + }); + }; + + t.context.watchForDirs = ( target, options, handler ) => { + const isDirEvent = event => event.endsWith ( 'Dir' ); + t.context.watch ( target, options, handler, isDirEvent ); + }; + + t.context.watchForFiles = ( target, options, handler ) => { + const isFileEvent = event => !event.endsWith ( 'Dir' ); + t.context.watch ( target, options, handler, isFileEvent ); + }; + + t.context.watchReset = () => { + t.context.events.length = 0; + t.context.changes.length = 0; + }; + + t.context.wait = {}; + + t.context.wait.close = () => { + return t.context.watcher._closeWait; + }; + + t.context.wait.ready = () => { + return t.context.watcher._readyWait; + }; + + t.context.wait.time = () => delay ( 1000 ); + + t.context.wait.longtime = () => delay ( 2000 ); + + t.context.wait.longlongtime = () => delay ( 3000 ); + + t.context.tree = TREES.pop (); + +}; + +const afterEach = t => { + + t.context.watcher.close (); + +}; + +const withContext = fn => { + + return async t => { + + await beforeEach ( t ); + + await fn ( t ); + + await afterEach ( t ); + + }; + +}; + +/* EXPORT */ + +export {before, beforeEach, afterEach, withContext}; diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..317ae99 --- /dev/null +++ b/test/index.js @@ -0,0 +1,2218 @@ + +/* IMPORT */ + +import {describe} from 'fava'; +import {execSync} from 'node:child_process'; +import {HAS_NATIVE_RECURSION} from '../dist/constants.js'; +import {before, withContext} from './hooks.js'; + +/* MAIN */ + +describe ( 'Watcher', () => { + + describe.before ( before ); + + describe ( 'watching files', it => { + + it ( 'should watch a single non-existent file inside a directory', withContext ( async t => { + const file = 'home/a/file_missing' + Math.random (); + t.context.watch ( file, { debounce: 0 } ); + await t.context.wait.ready (); + t.context.hasWatchObjects ( 0, 1, 0 ); + t.context.deepEqualResults ( [], [] ); + t.context.tree.newFile ( file ); + await t.context.wait.time (); + t.context.hasWatchObjects ( 0, 0, 2 ); + t.context.deepEqualResults ( ['add'], [file] ); + t.context.tree.remove ( file ); + await t.context.wait.time (); + t.context.hasWatchObjects ( 0, 1, 1 ); + t.context.deepEqualResults ( ['unlink'], [file] ); + t.context.tree.newFile ( file ); + await t.context.wait.time (); + t.context.hasWatchObjects ( 0, 0, 2 ); + t.context.deepEqualResults ( ['add'], [file] ); + t.context.tree.modify ( file ); + await t.context.wait.time (); + t.context.hasWatchObjects ( 0, 0, 2 ); + t.context.deepEqualResults ( ['change'], [file] ); + })); + + it ( 'should watch a single non-existent file inside a non-existent directory', withContext ( async t => { + const dir = 'home/a/dir_missing' + Math.random (); + const file = dir + '/file_missing' + Math.random (); + t.context.watch ( file, { debounce: 0, pollingInterval: 100 } ); + await t.context.wait.ready (); + t.context.hasWatchObjects ( 1, 0, 0 ); + t.context.deepEqualResults ( [], [] ); + t.context.tree.newFile ( file ); + await t.context.wait.time (); + t.context.hasWatchObjects ( 0, 0, 2 ); + t.context.deepEqualResults ( ['add'], [file] ); + t.context.tree.remove ( dir ); + await t.context.wait.time (); + t.context.hasWatchObjects ( 1, 0, 0 ); + t.context.deepEqualResults ( ['unlink'], [file] ); + t.context.tree.newFile ( file ); + await t.context.wait.time (); + t.context.hasWatchObjects ( 0, 0, 2 ); + t.context.deepEqualResults ( ['add'], [file] ); + t.context.tree.modify ( file ); + await t.context.wait.time (); + t.context.hasWatchObjects ( 0, 0, 2 ); + t.context.deepEqualResults ( ['change'], [file] ); + })); + + it ( 'should watch a single file', withContext ( async t => { + const file1 = 'home/a/file1'; + const file2 = 'home/a/file2'; + t.context.watch ( file1, { debounce: 0, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.hasWatchObjects ( 0, 0, 2 ); + t.context.tree.modify ( file1 ); + t.context.tree.modify ( file2 ); + t.context.tree.modify ( file1, 100 ); + t.context.tree.modify ( file2, 100 ); + t.context.tree.modify ( file1, 200 ); + t.context.tree.modify ( file2, 200 ); + await t.context.wait.time (); + t.context.deepEqualChanges ( [file1, file1, file1] ); + })); + + it ( 'should watch multiple files', withContext ( async t => { + const file1 = 'home/a/file1'; + const file2 = 'home/a/file2'; + t.context.watch ( [file1, file2], { debounce: 0, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.hasWatchObjects ( 0, 0, 3 ); + t.context.tree.modify ( 'home/a/file1' ); + t.context.tree.modify ( 'home/a/file2', 100 ); + await t.context.wait.time (); + t.context.deepEqualChanges ( [file1, file2] ); + })); + + it ( 'should watch all files inside a directory', withContext ( async t => { + const dir = 'home/a'; + const file1 = 'home/a/file1'; + const file2 = 'home/a/file2'; + t.context.watch ( dir, { debounce: 0, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.hasWatchObjects ( 0, 0, 3 ); + t.context.tree.modify ( 'home/a/file1' ); + t.context.tree.modify ( 'home/a/file2', 100 ); + await t.context.wait.time (); + t.context.deepEqualChanges ( [file1, file2] ); + })); + + it ( 'should watch new files inside a directory', withContext ( async t => { + const dir = 'home/a'; + const newfile1 = 'home/a/newfile' + Math.random (); + const newfile2 = 'home/a/newfile' + Math.random (); + t.context.watch ( dir, { debounce: 0, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.hasWatchObjects ( 0, 0, 3 ); + t.context.tree.newFile ( newfile1 ); + t.context.tree.newFile ( newfile2 ); + await t.context.wait.time (); + t.context.deepEqualUnorderedChanges ( [newfile1, newfile2] ); + })); + + it ( 'should watch new files inside an initially empty directory', withContext ( async t => { + const dir = 'home/empty'; + const newfile1 = 'home/empty/newfile' + Math.random (); + const newfile2 = 'home/empty/newfile' + Math.random (); + t.context.watch ( dir, { debounce: 0, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.hasWatchObjects ( 0, 0, 3 ); + t.context.tree.newFile ( newfile1 ); + t.context.tree.newFile ( newfile2 ); + await t.context.wait.time (); + t.context.deepEqualUnorderedChanges ( [newfile1, newfile2] ); + })); + + it ( 'should watch new files inside a new directory', withContext ( async t => { + const dir = 'home/a'; + const newdir = 'home/a/newdir' + Math.random (); + const newfile1 = newdir + '/newfile' + Math.random (); + const newfile2 = newdir + '/newfile' + Math.random (); + t.context.watch ( dir, { debounce: 0, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.tree.newFile ( newfile1 ); + t.context.tree.newFile ( newfile2 ); + await t.context.wait.time (); + t.context.deepEqualUnorderedChanges ( [newdir, newfile1, newfile2] ); + })); + + it ( 'should watch all files inside a deep directory', withContext ( async t => { + const dir = 'home'; + const file1 = 'home/a/file1'; + const file2 = 'home/a/file2'; + t.context.watch ( dir, { debounce: 0, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.tree.modify ( 'home/a/file1' ); + t.context.tree.modify ( 'home/a/file2', 100 ); + await t.context.wait.time (); + t.context.deepEqualChanges ( [file1, file2] ); + })); + + it ( 'should watch new files inside a deep directory', withContext ( async t => { + const dir = 'home'; + const newfile1 = 'home/a/newfile' + Math.random (); + const newfile2 = 'home/a/newfile' + Math.random (); + t.context.watch ( dir, { debounce: 0, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.tree.newFile ( newfile1 ); + t.context.tree.newFile ( newfile2 ); + await t.context.wait.time (); + t.context.deepEqualUnorderedChanges ( [newfile1, newfile2] ); + })); + + it ( 'should watch new files inside an initially empty deep directory', withContext ( async t => { + const dir = 'home'; + const newfile1 = 'home/empty/newfile' + Math.random (); + const newfile2 = 'home/empty/newfile' + Math.random (); + t.context.watch ( dir, { debounce: 0, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.hasWatchObjects ( 0, 0, 3 ); + t.context.tree.newFile ( newfile1 ); + t.context.tree.newFile ( newfile2 ); + await t.context.wait.time (); + t.context.deepEqualUnorderedChanges ( [newfile1, newfile2] ); + })); + + it ( 'should watch (touched) new files inside an initially empty deep directory', withContext ( async t => { + const dir = 'home'; + const newfile1 = 'home/empty/newfile' + Math.random (); + const newfile2 = 'home/empty/newfile' + Math.random (); + t.context.watch ( dir, { debounce: 0, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.hasWatchObjects ( 0, 0, 3 ); + execSync ( `touch "${t.context.tree.path ( newfile1 )}"` ); + execSync ( `touch "${t.context.tree.path ( newfile2 )}"` ); + await t.context.wait.time (); + t.context.deepEqualUnorderedChanges ( [newfile1, newfile2] ); + })); + + it ( 'should watch new files inside a new deep directory', withContext ( async t => { + const dir = 'home'; + const newdir = 'home/a/newdir' + Math.random (); + const newfile1 = newdir + '/newfile' + Math.random (); + const newfile2 = newdir + '/newfile' + Math.random (); + t.context.watch ( dir, { debounce: 0, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.tree.newFile ( newfile1 ); + t.context.tree.newFile ( newfile2 ); + await t.context.wait.time (); + t.context.deepEqualUnorderedChanges ( [newdir, newfile1, newfile2] ); + })); + + it ( 'should deduplicate events', withContext ( async t => { + const file = 'home/a/file2'; + t.context.watch ( file, { debounce: 300, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.tree.modify ( file ); + t.context.tree.modify ( file, 50 ); + t.context.tree.modify ( file, 100 ); + await t.context.wait.time (); + t.context.deepEqualChanges ( [file] ); + })); + + it ( 'should deduplicate events inside a directory', withContext ( async t => { + const dir = 'home/a'; + const file = 'home/a/file1'; + t.context.watch ( dir, { debounce: 300, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.tree.modify ( file ); + t.context.tree.modify ( file, 50 ); + t.context.tree.modify ( file, 100 ); + await t.context.wait.time (); + t.context.deepEqualResults ( ['change'], [file] ); + })); + + it ( 'should deduplicate events inside a deep directory', withContext ( async t => { + const dir = 'home'; + const file = 'home/a/file1'; + t.context.watch ( dir, { debounce: 300, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.tree.modify ( file ); + t.context.tree.modify ( file, 50 ); + t.context.tree.modify ( file, 100 ); + await t.context.wait.time (); + t.context.deepEqualResults ( ['change'], [file] ); + })); + + }); + + describe ( 'watching directories', it => { + + it ( 'should watch a single non-existent directory inside a directory', withContext ( async t => { + const dir = 'home/a/dir_missing' + Math.random (); + t.context.watch ( dir, { debounce: 0 } ); + await t.context.wait.ready (); + t.context.hasWatchObjects ( 0, 1, 0 ); + t.context.deepEqualResults ( [], [] ); + t.context.tree.newDir ( dir ); + await t.context.wait.time (); + t.context.hasWatchObjects ( 0, 0, 3 ); + t.context.deepEqualResults ( ['addDir'], [dir] ); + t.context.tree.remove ( dir ); + await t.context.wait.time (); + t.context.hasWatchObjects ( 0, 1, 1 ); + t.context.deepEqualResults ( ['unlinkDir'], [dir] ); + t.context.tree.newDir ( dir ); + await t.context.wait.time (); + t.context.hasWatchObjects ( 0, 0, 3 ); + t.context.deepEqualResults ( ['addDir'], [dir] ); + })); + + it ( 'should watch a single non-existent directory inside a non-existent directory', withContext ( async t => { + const pdir = 'home/a/dir_missing' + Math.random (); + const dir = pdir + '/dir_missing' + Math.random (); + t.context.watch ( dir, { debounce: 0, pollingInterval: 100 } ); + await t.context.wait.ready (); + t.context.hasWatchObjects ( 1, 0, 0 ); + t.context.deepEqualResults ( [], [] ); + t.context.tree.newDir ( dir ); + await t.context.wait.time (); + t.context.hasWatchObjects ( 0, 0, 3 ); + t.context.deepEqualResults ( ['addDir'], [dir] ); + t.context.tree.remove ( pdir ); + await t.context.wait.time (); + t.context.hasWatchObjects ( 1, 0, 0 ); + t.context.deepEqualResults ( ['unlinkDir'], [dir] ); + t.context.tree.newDir ( dir ); + await t.context.wait.time (); + t.context.hasWatchObjects ( 0, 0, 3 ); + t.context.deepEqualResults ( ['addDir'], [dir] ); + })); + + it ( 'should watch new directories inside a directory', withContext ( async t => { + const dir = 'home/a'; + const newdir1 = 'home/a/dir1' + Math.random (); + const newdir2 = 'home/a/dir2' + Math.random (); + t.context.watch ( dir, { debounce: 0, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.hasWatchObjects ( 0, 0, 3 ); + t.context.tree.newDir ( newdir1 ); + t.context.tree.newDir ( newdir2 ); + await t.context.wait.time (); + t.context.hasWatchObjects ( 0, 0, 3 ); + t.context.deepEqualUnorderedChanges ( [newdir1, newdir2] ); + })); + + it ( 'should watch new directories inside an initially empty directory', withContext ( async t => { + const dir = 'home/empty'; + const newdir1 = 'home/empty/dir1' + Math.random (); + const newdir2 = 'home/empty/dir2' + Math.random (); + t.context.watch ( dir, { debounce: 0, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.hasWatchObjects ( 0, 0, 3 ); + t.context.tree.newDir ( newdir1 ); + t.context.tree.newDir ( newdir2 ); + await t.context.wait.time (); + t.context.hasWatchObjects ( 0, 0, 3 ); + t.context.deepEqualUnorderedChanges ( [newdir1, newdir2] ); + })); + + it ( 'should watch new directories inside a new directory', withContext ( async t => { + const dir = 'home/a'; + const newdir0 = 'home/a/newdir' + Math.random (); + const newdir1 = newdir0 + '/newdir' + Math.random (); + const newdir2 = newdir0 + '/newdir' + Math.random (); + t.context.watch ( dir, { debounce: 0, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.tree.newDir ( newdir1 ); + t.context.tree.newDir ( newdir2 ); + await t.context.wait.time (); + t.context.deepEqualUnorderedChanges ( [newdir0, newdir1, newdir2] ); + })); + + it ( 'should watch new directories inside a deep directory', withContext ( async t => { + const dir = 'home'; + const newdir1 = 'home/a/dir1' + Math.random (); + const newdir2 = 'home/a/dir2' + Math.random (); + t.context.watch ( dir, { debounce: 0, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.tree.newDir ( newdir1 ); + t.context.tree.newDir ( newdir2 ); + await t.context.wait.time (); + t.context.deepEqualUnorderedChanges ( [newdir1, newdir2] ); + })); + + it ( 'should watch new directories inside an initially empty deep directory', withContext ( async t => { + const dir = 'home'; + const newdir1 = 'home/empty/dir1' + Math.random (); + const newdir2 = 'home/empty/dir2' + Math.random (); + t.context.watch ( dir, { debounce: 0, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.tree.newDir ( newdir1 ); + t.context.tree.newDir ( newdir2 ); + await t.context.wait.time (); + t.context.deepEqualUnorderedChanges ( [newdir1, newdir2] ); + })); + + it ( 'should watch new directories inside a new deep directory', withContext ( async t => { + const dir = 'home'; + const newdir0 = 'home/a/newdir' + Math.random (); + const newdir1 = newdir0 + '/newdir' + Math.random (); + const newdir2 = newdir0 + '/newdir' + Math.random (); + t.context.watch ( dir, { debounce: 0, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.tree.newDir ( newdir1 ); + t.context.tree.newDir ( newdir2 ); + await t.context.wait.time (); + t.context.deepEqualUnorderedChanges ( [newdir0, newdir1, newdir2] ); + })); + + it ( 'should deduplicate events inside a directory', withContext ( async t => { + const dir = 'home/a'; + const newdir1 = 'home/a/newdir1' + Math.random (); + const newdir2 = 'home/a/newdir2' + Math.random (); + t.context.watch ( dir, { debounce: 300, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.tree.newDir ( newdir1 ); + t.context.tree.newDir ( newdir2 ); + t.context.tree.remove ( newdir1, 50 ); + t.context.tree.remove ( newdir2, 50 ); + t.context.tree.newDir ( newdir1, 100 ); + t.context.tree.newDir ( newdir2, 100 ); + await t.context.wait.time (); + t.context.deepEqualUnorderedResults ( ['addDir', 'addDir'], [newdir1, newdir2] ); + })); + + it ( 'should deduplicate events inside a deep directory', withContext ( async t => { + const dir = 'home'; + const newdir1 = 'home/a/newdir1' + Math.random (); + const newdir2 = 'home/a/newdir2' + Math.random (); + t.context.watch ( dir, { debounce: 300, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.tree.newDir ( newdir1 ); + t.context.tree.newDir ( newdir2 ); + t.context.tree.remove ( newdir1, 50 ); + t.context.tree.remove ( newdir2, 50 ); + t.context.tree.newDir ( newdir1, 100 ); + t.context.tree.newDir ( newdir2, 100 ); + await t.context.wait.time (); + t.context.deepEqualUnorderedResults ( ['addDir', 'addDir'], [newdir1, newdir2] ); + })); + + it ( 'should keep watching after removal of sub directory', withContext ( async t => { + const home = 'home'; + const dir = t.context.tree.path ( 'home/e/sub' ); + const file1 = t.context.tree.path ( 'home/e/file1' ); + const file2 = t.context.tree.path ( 'home/e/file2' ); + const subfile = t.context.tree.path ( 'home/e/sub/file1' ); + const changes = []; + t.context.watch ( home, { debounce: 0, ignoreInitial: true, recursive: true } ); + t.context.watcher.on ( 'all', ( event, name ) => { + if ( name === dir || name === file1 || name === file2 ) { + changes.push ( name ); + } + }); + await t.context.wait.ready (); + t.context.tree.remove ( 'home/e/sub', 50 ); + t.context.tree.modify ( 'home/e/file1', 100 ); + t.context.tree.modify ( 'home/e/file2', 150 ); + await t.context.wait.time (); + t.context.deepEqualUnorderedResults ( ['unlink', 'unlinkDir', 'change', 'change'], [subfile, dir, file1, file2] ); + })); + + it ( 'should close all eventual additional watchers added for recursiong when no longer needed', withContext ( async t => { + const home = 'home/a'; + const dir1 = 'home/a/sub1'; + const dir2 = dir1 + '/sub2'; + t.context.watch ( home, { debounce: 0, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.hasWatchObjects ( 0, 0, 3 ); + t.context.deepEqualResults ( [], [] ); + t.context.tree.newDir ( dir2 ); + await t.context.wait.time (); + t.context.hasWatchObjects ( 0, 0, HAS_NATIVE_RECURSION ? 3 : 5 ); + t.context.deepEqualUnorderedResults ( ['addDir', 'addDir'], [dir1, dir2] ); + t.context.tree.remove ( dir2 ); + await t.context.wait.time (); + t.context.hasWatchObjects ( 0, 0, HAS_NATIVE_RECURSION ? 3 : 4 ); + t.context.deepEqualResults ( ['unlinkDir'], [dir2] ); + t.context.tree.remove ( dir1); + await t.context.wait.time (); + t.context.hasWatchObjects ( 0, 0, 3 ); + t.context.deepEqualResults ( ['unlinkDir'], [dir1] ); + })); + + }); + + describe.todo ( 'watching symlinks' ); + + describe ( 'file events', it => { + + it ( 'should detect initial "add" for a single file', withContext ( async t => { + const file = 'home/a/file1'; + t.context.watchForFiles ( file, { debounce: 0 } ); + await t.context.wait.ready (); + await t.context.wait.time (); + t.context.deepEqualResults ( ['add'], [file] ); + })); + + it ( 'should detect initial "add" for multiple files', withContext ( async t => { + const file1 = 'home/a/file1'; + const file2 = 'home/a/file2'; + t.context.watchForFiles ( [file1, file2], { debounce: 0 } ); + await t.context.wait.ready (); + await t.context.wait.time (); + t.context.deepEqualUnorderedResults ( ['add', 'add'], [file1, file2] ); + })); + + it ( 'should detect initial "add" for all files inside a directory', withContext ( async t => { + const dir = 'home/a'; + const file1 = 'home/a/file1'; + const file2 = 'home/a/file2'; + t.context.watchForFiles ( dir, { debounce: 0 } ); + await t.context.wait.ready (); + await t.context.wait.time (); + t.context.deepEqualUnorderedResults ( ['add', 'add'], [file1, file2] ); + })); + + it ( 'should detect initial "add" for all files inside a deep directory', withContext ( async t => { + const dir = 'home/e'; + const file1 = 'home/e/file1'; + const file2 = 'home/e/file2'; + const filesub1 = 'home/e/sub/file1'; + t.context.watchForFiles ( dir, { debounce: 0, recursive: true } ); + await t.context.wait.ready (); + await t.context.wait.time (); + t.context.deepEqualUnorderedResults ( ['add', 'add', 'add'], [file1, file2, filesub1] ); + })); + + it ( 'should detect "add" when creating a new file inside a directory', withContext ( async t => { + const dir = 'home/a'; + const newfile = 'home/a/file1' + Math.random (); + t.context.watchForFiles ( dir, { debounce: 0, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.tree.newFile ( newfile ); + await t.context.wait.time (); + t.context.deepEqualResults ( ['add'], [newfile] ); + })); + + it ( 'should detect "add" when creating a new file inside a new directory', withContext ( async t => { + const dir = 'home/a'; + const newdir = 'home/a/newdir' + Math.random (); + const newfile = newdir + '/file1' + Math.random (); + t.context.watchForFiles ( dir, { debounce: 0, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.tree.newFile ( newfile ); + await t.context.wait.time (); + t.context.deepEqualResults ( ['add'], [newfile] ); + })); + + it ( 'should detect "add" when creating a new file inside a new deep directory', withContext ( async t => { + const dir = 'home'; + const newdir = 'home/a/newdir' + Math.random (); + const newfile = newdir + '/file1' + Math.random (); + t.context.watchForFiles ( dir, { debounce: 0, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.tree.newFile ( newfile ); + await t.context.wait.time (); + t.context.deepEqualResults ( ['add'], [newfile] ); + })); + + it ( 'should detect "add" when copying a file inside a directory', withContext ( async t => { + const dir = 'home/a'; + const file = 'home/a/file1'; + const copyfile = file + Math.random (); + t.context.watchForFiles ( dir, { debounce: 0, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.tree.copy ( file, copyfile ); + await t.context.wait.time (); + t.context.deepEqualResults ( ['add'], [copyfile] ); + })); + + it ( 'should detect "add" when copying a file inside a deep directory', withContext ( async t => { + const dir = 'home'; + const file = 'home/a/file1'; + const copyfile = file + Math.random (); + t.context.watchForFiles ( dir, { debounce: 0, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.tree.copy ( file, copyfile ); + await t.context.wait.time (); + t.context.deepEqualResults ( ['add'], [copyfile] ); + })); + + it ( 'should detect "add" when copying a parent directory', withContext ( async t => { + const home = 'home/e'; + const dir = 'home/e/sub'; + const copydir = dir + Math.random (); + const copyfile = copydir + '/file1'; + t.context.watchForFiles ( home, { debounce: 0, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.tree.copy ( dir, copydir ); + await t.context.wait.time (); + t.context.deepEqualResults ( ['add'], [copyfile] ); + })); + + it ( 'should detect "add" when copying a deep parent directory', withContext ( async t => { + const home = 'home'; + const dir = 'home/e/sub'; + const copydir = dir + Math.random (); + const copyfile = copydir + '/file1'; + t.context.watchForFiles ( home, { debounce: 0, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.tree.copy ( dir, copydir ); + await t.context.wait.time (); + t.context.deepEqualResults ( ['add'], [copyfile] ); + })); + + it ( 'should detect "change" when modifying a file inside a directory', withContext ( async t => { + const dir = 'home/a'; + const file = 'home/a/file1'; + t.context.watchForFiles ( dir, { debounce: 0, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.tree.modify ( file ); + await t.context.wait.time (); + t.context.deepEqualResults ( ['change'], [file] ); + })); + + it ( 'should detect "change" when modifying a file inside a deep directory', withContext ( async t => { + const dir = 'home'; + const file = 'home/a/file1'; + t.context.watchForFiles ( dir, { debounce: 0, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.tree.modify ( file ); + await t.context.wait.time (); + t.context.deepEqualResults ( ['change'], [file] ); + })); + + it ( 'should detect "change" when renaming a non-empty file and rerenaming it', withContext ( async t => { + const dir = 'home/a'; + const file = 'home/a/file1'; + const filealt = 'home/a/file1_alt'; + t.context.watchForFiles ( dir, { debounce: 300, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.tree.modify ( file ); + t.context.tree.rename ( file, filealt ); + t.context.tree.rename ( filealt, file ); + await t.context.wait.time (); + t.context.deepEqualResults ( ['change'], [file] ); + })); + + it ( 'should detect "unlink" when removing a single file', withContext ( async t => { + const file = 'home/a/file1'; + t.context.watchForFiles ( file, { debounce: 0, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.hasWatchObjects ( 0, 0, 2 ); + t.context.tree.remove ( file ); + await t.context.wait.time (); + t.context.hasWatchObjects ( 0, 1, 1 ); + t.context.deepEqualResults ( ['unlink'], [file] ); + })); + + it ( 'should detect "unlink" and "add" when removing a single file and much later recreating it', withContext ( async t => { + const file = 'home/a/file1'; + t.context.watchForFiles ( file, { debounce: 0, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.hasWatchObjects ( 0, 0, 2 ); + t.context.tree.remove ( file ); + await t.context.wait.time (); + t.context.hasWatchObjects ( 0, 1, 1 ); + t.context.deepEqualResults ( ['unlink'], [file] ); + t.context.tree.newFile ( file ); + await t.context.wait.time (); + t.context.hasWatchObjects ( 0, 0, 2 ); + t.context.deepEqualResults ( ['add'], [file] ); + })); + + it ( 'should detect "unlink" when removing a file inside a directory', withContext ( async t => { + const dir = 'home/a'; + const file = 'home/a/file1'; + t.context.watchForFiles ( dir, { debounce: 0, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.tree.remove ( file ); + await t.context.wait.time (); + t.context.deepEqualResults ( ['unlink'], [file] ); + })); + + it ( 'should detect "unlink" when removing a file inside a deep directory', withContext ( async t => { + const dir = 'home'; + const file = 'home/a/file1'; + t.context.watchForFiles ( dir, { debounce: 0, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.tree.remove ( file ); + await t.context.wait.time (); + t.context.deepEqualResults ( ['unlink'], [file] ); + })); + + it ( 'should detect "unlink" when removing a parent directory', withContext ( async t => { + const dir = 'home'; + const file1 = 'home/a/file1'; + const file2 = 'home/a/file2'; + t.context.watchForFiles ( dir, { debounce: 0, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.tree.remove ( 'home/a' ); + await t.context.wait.time (); + t.context.deepEqualUnorderedResults ( ['unlink', 'unlink'], [file1, file2] ); + })); + + it ( 'should detect "unlink" when removing a parent directory of the watcher', withContext ( async t => { + const dir = 'home/e/sub'; + const file = 'home/e/sub/file1'; + t.context.watchForFiles ( dir, { debounce: 0, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.hasWatchObjects ( 0, 0, 3 ) + t.context.tree.remove ( 'home/e' ); + await t.context.wait.time (); + t.context.hasWatchObjects ( 1, 0, 0 ); + t.context.deepEqualResults ( ['unlink'], [file] ); + })); + + it ( 'should detect "unlink" and "add" when renaming a file inside a directory', withContext ( async t => { + const dir = 'home/a'; + const file1 = 'home/a/file1'; + const file1alt = 'home/a/file1_alt'; + t.context.watchForFiles ( dir, { debounce: 0, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.tree.rename ( file1, file1alt ); + await t.context.wait.time (); + t.context.deepEqualUnorderedResults ( ['unlink', 'add'], [file1, file1alt] ); + })); + + it ( 'should detect "unlink" and "add" when renaming a file inside a deep directory', withContext ( async t => { + const dir = 'home'; + const file1 = 'home/a/file1'; + const file1alt = 'home/a/file1_alt'; + t.context.watchForFiles ( dir, { debounce: 0, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.tree.rename ( file1, file1alt ); + await t.context.wait.time (); + t.context.deepEqualUnorderedResults ( ['unlink', 'add'], [file1, file1alt] ); + })); + + it ( 'should detect "unlink" and "add" when renaming a parent directory', withContext ( async t => { + const dir = 'home'; + const file1 = 'home/a/file1'; + const file1alt = 'home/a_alt/file1'; + const file2 = 'home/a/file2'; + const file2alt = 'home/a_alt/file2'; + t.context.watchForFiles ( dir, { debounce: 300, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.tree.rename ( 'home/a', 'home/a_alt' ); + await t.context.wait.time (); + t.context.deepEqualUnorderedResults ( ['unlink', 'add', 'unlink', 'add'], [file1, file1alt, file2, file2alt] ); + })); + + it ( 'should detect a single "add" when creating a new file and modifying it', withContext ( async t => { + const dir = 'home/a'; + const newfile = 'home/a/file1' + Math.random (); + t.context.watchForFiles ( dir, { debounce: 300, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.tree.newFile ( newfile ); + t.context.tree.modify ( newfile ); + await t.context.wait.time (); + t.context.deepEqualResults ( ['add'], [newfile] ); + })); + + it ( 'should detect a single "change" when removing a file and creating it', withContext ( async t => { + const dir = 'home/a'; + const file = 'home/a/file1'; + t.context.watchForFiles ( dir, { debounce: 300, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.tree.remove ( file ); + t.context.tree.newFile ( file ); + await t.context.wait.time (); + t.context.deepEqualResults ( ['change'], [file] ); + })); + + it ( 'should detect a single "unlink" when modifying a file and removing it', withContext ( async t => { + const dir = 'home/a'; + const file = 'home/a/file1'; + t.context.watchForFiles ( dir, { debounce: 300, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.tree.modify ( file ); + t.context.tree.remove ( file ); + await t.context.wait.time (); + t.context.deepEqualResults ( ['unlink'], [file] ); + })); + + it ( 'should detect nothing when creating a new file and removing it', withContext ( async t => { + const dir = 'home/a'; + const newfile = 'home/a/file1' + Math.random (); + t.context.watchForFiles ( dir, { debounce: 300, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.tree.newFile ( newfile ); + t.context.tree.remove ( newfile ); + await t.context.wait.time (); + t.context.deepEqualResults ( [], [] ); + })); + + it ( 'should detect nothing when renaming an empty file and rerenaming it', withContext ( async t => { + const dir = 'home/a'; + const file = 'home/a/file1'; + const filealt = 'home/a/file1_alt'; + t.context.watchForFiles ( dir, { debounce: 300, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.tree.rename ( file, filealt ); + t.context.tree.rename ( filealt, file ); + await t.context.wait.time (); + t.context.deepEqualResults ( [], [] ); + })); + + it ( 'should detect nothing when renaming a parent directory and rerenaming it', withContext ( async t => { + const dir = 'home/a'; + const diralt = 'home/a_alt'; + t.context.watchForFiles ( dir, { debounce: 300, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.tree.rename ( dir, diralt ); + t.context.tree.rename ( diralt, dir ); + await t.context.wait.time (); + t.context.deepEqualResults ( [], [] ); + })); + + it ( 'should detect "unlink" when removing a file and creating a directory of the same name', withContext ( async t => { + const dir = 'home/a'; + const file = 'home/a/file1'; + t.context.watchForFiles ( dir, { debounce: 300, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.tree.remove ( file ); + t.context.tree.newDir ( file ); + await t.context.wait.time (); + t.context.deepEqualResults ( ['unlink'], [file] ); + })); + + it ( 'should detect "add" when removing a directory and creating a file of the same name', withContext ( async t => { + const dir = 'home'; + const file = 'home/a'; + t.context.watchForFiles ( dir, { debounce: 300, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.tree.remove ( file ); + t.context.tree.newFile ( file ); + await t.context.wait.time (); + t.context.deepEqualResults ( ['add'], [file] ); + })); + + it ( 'should detect "change" when replacing a parent directory with another one of the same name', withContext ( async t => { + const dir = 'home'; + t.context.watchForFiles ( dir, { debounce: 300, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.tree.remove ( 'home/a' ); + t.context.tree.copy ( 'home/b', 'home/a' ); + await t.context.wait.time (); + t.context.deepEqualUnorderedResults ( ['change', 'change'], ['home/a/file1', 'home/a/file2'] ); + })); + + it ( 'should be able to handle many "unlink" events', withContext ( async t => { + const dir = 'home/a'; + const files = t.context.tree.newFiles ( dir, 100 ); + t.context.watchForFiles ( dir, { debounce: 0, ignoreInitial: true } ); + await t.context.wait.ready (); + await t.context.wait.longtime () + files.forEach ( file => t.context.tree.remove ( file ) ); + await t.context.wait.longtime () + t.is ( t.context.events.length, 100 ); + })); + + }); + + describe ( 'file events (with renames)', it => { + + it ( 'should detect initial "add" for a single file', withContext ( async t => { + const file = 'home/a/file1'; + t.context.watchForFiles ( file, { debounce: 0, renameDetection: true } ); + await t.context.wait.ready (); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['add'], [file] ); + })); + + it ( 'should detect initial "add" for multiple files', withContext ( async t => { + const file1 = 'home/a/file1'; + const file2 = 'home/a/file2'; + t.context.watchForFiles ( [file1, file2], { debounce: 0, renameDetection: true } ); + await t.context.wait.ready (); + await t.context.wait.longtime (); + t.context.deepEqualUnorderedResults ( ['add', 'add'], [file1, file2] ); + })); + + it ( 'should detect initial "add" for all files inside a directory', withContext ( async t => { + const dir = 'home/a'; + const file1 = 'home/a/file1'; + const file2 = 'home/a/file2'; + t.context.watchForFiles ( dir, { debounce: 0, renameDetection: true } ); + await t.context.wait.ready (); + await t.context.wait.longtime (); + t.context.deepEqualUnorderedResults ( ['add', 'add'], [file1, file2] ); + })); + + it ( 'should detect initial "add" for all files inside a deep directory', withContext ( async t => { + const dir = 'home/e'; + const file1 = 'home/e/file1'; + const file2 = 'home/e/file2'; + const filesub1 = 'home/e/sub/file1'; + t.context.watchForFiles ( dir, { debounce: 0, recursive: true, renameDetection: true } ); + await t.context.wait.ready (); + await t.context.wait.longtime (); + t.context.deepEqualUnorderedResults ( ['add', 'add', 'add'], [file1, file2, filesub1] ); + })); + + it ( 'should detect "add" when creating a new file inside a directory', withContext ( async t => { + const dir = 'home/a'; + const newfile = 'home/a/file1' + Math.random (); + t.context.watchForFiles ( dir, { debounce: 0, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.newFile ( newfile ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['add'], [newfile] ); + })); + + it ( 'should detect "add" when creating a new file inside a new directory', withContext ( async t => { + const dir = 'home/a'; + const newdir = 'home/a/newdir' + Math.random (); + const newfile = newdir + '/file1' + Math.random (); + t.context.watchForFiles ( dir, { debounce: 0, ignoreInitial: true, recursive: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.newFile ( newfile ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['add'], [newfile] ); + })); + + it ( 'should detect "add" when creating a new file inside a new deep directory', withContext ( async t => { + const dir = 'home'; + const newdir = 'home/a/newdir' + Math.random (); + const newfile = newdir + '/file1' + Math.random (); + t.context.watchForFiles ( dir, { debounce: 0, ignoreInitial: true, recursive: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.newFile ( newfile ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['add'], [newfile] ); + })); + + it ( 'should detect "add" when copying a file inside a directory', withContext ( async t => { + const dir = 'home/a'; + const file = 'home/a/file1'; + const copyfile = file + Math.random (); + t.context.watchForFiles ( dir, { debounce: 0, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.copy ( file, copyfile ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['add'], [copyfile] ); + })); + + it ( 'should detect "add" when copying a file inside a deep directory', withContext ( async t => { + const dir = 'home'; + const file = 'home/a/file1'; + const copyfile = file + Math.random (); + t.context.watchForFiles ( dir, { debounce: 0, ignoreInitial: true, recursive: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.copy ( file, copyfile ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['add'], [copyfile] ); + })); + + it ( 'should detect "add" when copying a parent directory', withContext ( async t => { + const home = 'home/e'; + const dir = 'home/e/sub'; + const copydir = dir + Math.random (); + const copyfile = copydir + '/file1'; + t.context.watchForFiles ( home, { debounce: 0, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.copy ( dir, copydir ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['add'], [copyfile] ); + })); + + it ( 'should detect "add" when copying a deep parent directory', withContext ( async t => { + const home = 'home'; + const dir = 'home/e/sub'; + const copydir = dir + Math.random (); + const copyfile = copydir + '/file1'; + t.context.watchForFiles ( home, { debounce: 0, ignoreInitial: true, recursive: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.copy ( dir, copydir ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['add'], [copyfile] ); + })); + + it ( 'should detect "change" when modifying a file inside a directory', withContext ( async t => { + const dir = 'home/a'; + const file = 'home/a/file1'; + t.context.watchForFiles ( dir, { debounce: 0, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.modify ( file ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['change'], [file] ); + })); + + it ( 'should detect "change" when modifying a file inside a deep directory', withContext ( async t => { + const dir = 'home'; + const file = 'home/a/file1'; + t.context.watchForFiles ( dir, { debounce: 0, ignoreInitial: true, recursive: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.modify ( file ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['change'], [file] ); + })); + + it ( 'should detect "change" when renaming a non-empty file and rerenaming it', withContext ( async t => { + const dir = 'home/a'; + const file = 'home/a/file1'; + const filealt = 'home/a/file1_alt'; + t.context.watchForFiles ( dir, { debounce: 300, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.modify ( file ); + t.context.tree.rename ( file, filealt ); + t.context.tree.rename ( filealt, file ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['change'], [file] ); + })); + + it ( 'should detect "unlink" when removing a single file', withContext ( async t => { + const file = 'home/a/file1'; + t.context.watchForFiles ( file, { debounce: 0, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.hasWatchObjects ( 0, 0, 2 ); + t.context.tree.remove ( file ); + await t.context.wait.longtime (); + t.context.hasWatchObjects ( 0, 1, 1 ); + t.context.deepEqualResults ( ['unlink'], [file] ); + })); + + it ( 'should detect "unlink" and "add" when removing a single file and much later recreating it', withContext ( async t => { + const file = 'home/a/file1'; + t.context.watchForFiles ( file, { debounce: 0, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.hasWatchObjects ( 0, 0, 2 ); + t.context.tree.remove ( file ); + await t.context.wait.longtime (); + t.context.hasWatchObjects ( 0, 1, 1 ); + t.context.deepEqualResults ( ['unlink'], [file] ); + t.context.tree.newFile ( file ); + await t.context.wait.longlongtime (); + t.context.hasWatchObjects ( 0, 0, 2 ); + t.context.deepEqualResults ( ['add'], [file] ); + })); + + it ( 'should detect "unlink" when removing a file inside a directory', withContext ( async t => { + const dir = 'home/a'; + const file = 'home/a/file1'; + t.context.watchForFiles ( dir, { debounce: 0, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.remove ( file ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['unlink'], [file] ); + })); + + it ( 'should detect "unlink" when removing a file inside a deep directory', withContext ( async t => { + const dir = 'home'; + const file = 'home/a/file1'; + t.context.watchForFiles ( dir, { debounce: 0, ignoreInitial: true, recursive: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.remove ( file ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['unlink'], [file] ); + })); + + it ( 'should detect "unlink" when removing a parent directory', withContext ( async t => { + const dir = 'home'; + const file1 = 'home/a/file1'; + const file2 = 'home/a/file2'; + t.context.watchForFiles ( dir, { debounce: 0, ignoreInitial: true, recursive: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.remove ( 'home/a' ); + await t.context.wait.longtime (); + t.context.deepEqualUnorderedResults ( ['unlink', 'unlink'], [file1, file2] ); + })); + + it ( 'should detect "unlink" when removing a parent directory of the watcher', withContext ( async t => { + const dir = 'home/e/sub'; + const file = 'home/e/sub/file1'; + t.context.watchForFiles ( dir, { debounce: 0, ignoreInitial: true, recursive: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.hasWatchObjects ( 0, 0, 3 ) + t.context.tree.remove ( 'home/e' ); + await t.context.wait.longtime (); + t.context.hasWatchObjects ( 1, 0, 0 ); + t.context.deepEqualResults ( ['unlink'], [file] ); + })); + + it ( 'should detect "rename" when renaming a file inside a directory', withContext ( async t => { + const dir = 'home/a'; + const file1 = 'home/a/file1'; + const file1alt = 'home/a/file1_alt'; + t.context.watchForFiles ( dir, { debounce: 0, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.rename ( file1, file1alt ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['rename'], [[file1, file1alt]] ); + })); + + it ( 'should detect "rename" when renaming a file inside a deep directory', withContext ( async t => { + const dir = 'home'; + const file1 = 'home/a/file1'; + const file1alt = 'home/a/file1_alt'; + t.context.watchForFiles ( dir, { debounce: 0, ignoreInitial: true, recursive: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.rename ( file1, file1alt ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['rename'], [[file1, file1alt]] ); + })); + + it ( 'should detect "rename" when renaming a parent directory', withContext ( async t => { + const dir = 'home'; + const file1 = 'home/a/file1'; + const file1alt = 'home/a_alt/file1'; + const file2 = 'home/a/file2'; + const file2alt = 'home/a_alt/file2'; + t.context.watchForFiles ( dir, { debounce: 300, ignoreInitial: true, recursive: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.rename ( 'home/a', 'home/a_alt' ); + await t.context.wait.longlongtime (); + t.context.deepEqualUnorderedResults ( ['rename', 'rename'], [[file1, file1alt], [file2, file2alt]] ); + })); + + it ( 'should detect "rename" when renaming a file inside a directory case-sensitively', withContext ( async t => { + const dir = 'home/a'; + const file1 = 'home/a/file1'; + const File1 = 'home/a/File1'; + t.context.watchForFiles ( dir, { debounce: 0, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.rename ( file1, File1 ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['rename'], [[file1, File1]] ); + })); + + it ( 'should detect "rename" when renaming a file inside a deep directory case-sensitively', withContext ( async t => { + const dir = 'home'; + const file1 = 'home/a/file1'; + const File1 = 'home/a/File1'; + t.context.watchForFiles ( dir, { debounce: 0, ignoreInitial: true, recursive: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.rename ( file1, File1 ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['rename'], [[file1, File1]] ); + })); + + it ( 'should detect "rename" when renaming a parent directory case-sensitively', withContext ( async t => { + const dir = 'home'; + const file1 = 'home/a/file1'; + const File1 = 'home/A/file1'; + const file2 = 'home/a/file2'; + const File2 = 'home/A/file2'; + t.context.watchForFiles ( dir, { debounce: 300, ignoreInitial: true, recursive: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.rename ( 'home/a', 'home/A' ); + await t.context.wait.longlongtime (); + t.context.deepEqualUnorderedResults ( ['rename', 'rename'], [[file1, File1], [file2, File2]] ); + })); + + it ( 'should detect a single "add" when creating a new file and modifying it', withContext ( async t => { + const dir = 'home/a'; + const newfile = 'home/a/file1' + Math.random (); + t.context.watchForFiles ( dir, { debounce: 300, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.newFile ( newfile ); + t.context.tree.modify ( newfile ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['add'], [newfile] ); + })); + + it ( 'should detect a single "change" when removing a file and creating it', withContext ( async t => { + const dir = 'home/a'; + const file = 'home/a/file1'; + t.context.watchForFiles ( dir, { debounce: 300, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.remove ( file ); + t.context.tree.newFile ( file ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['change'], [file] ); + })); + + it ( 'should detect a single "unlink" when modifying a file and removing it', withContext ( async t => { + const dir = 'home/a'; + const file = 'home/a/file1'; + t.context.watchForFiles ( dir, { debounce: 300, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.modify ( file ); + t.context.tree.remove ( file ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['unlink'], [file] ); + })); + + it ( 'should detect nothing when renaming an empty file and rerenaming it', withContext ( async t => { + const dir = 'home/a'; + const file = 'home/a/file1'; + const filealt = 'home/a/file1_alt'; + t.context.watchForFiles ( dir, { debounce: 300, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.rename ( file, filealt ); + t.context.tree.rename ( filealt, file ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( [], [] ); + })); + + it ( 'should detect nothing when renaming an empty file case-sensitively and rerenaming it', withContext ( async t => { + const dir = 'home/a'; + const file = 'home/a/file1'; + const File = 'home/a/File1'; + t.context.watchForFiles ( dir, { debounce: 300, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.rename ( file, File ); + t.context.tree.rename ( File, file ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( [], [] ); + })); + + it ( 'should detect nothing when creating a new file and removing it', withContext ( async t => { + const dir = 'home/a'; + const newfile = 'home/a/file1' + Math.random (); + t.context.watchForFiles ( dir, { debounce: 300, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.newFile ( newfile ); + t.context.tree.remove ( newfile ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( [], [] ); + })); + + it ( 'should detect nothing when creating a new file and removing it after a delay', withContext ( async t => { + const dir = 'home/a'; + const newfile = 'home/a/file' + Math.random (); + t.context.watchForFiles ( dir, { debounce: 300, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.newFile ( newfile ); + await t.context.wait.time (); + t.context.tree.remove ( newfile ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( [], [] ); + })); + + it ( 'should detect nothing when renaming a parent directory and rerenaming it', withContext ( async t => { + const dir = 'home/a'; + const diralt = 'home/a_alt'; + t.context.watchForFiles ( dir, { debounce: 300, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.rename ( dir, diralt ); + t.context.tree.rename ( diralt, dir ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( [], [] ); + })); + + it ( 'should detect nothing when renaming a parent directory case-sensitively and rerenaming it', withContext ( async t => { + const dir = 'home/a'; + const Dir = 'home/A'; + t.context.watchForFiles ( dir, { debounce: 300, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.rename ( dir, Dir ); + t.context.tree.rename ( Dir, dir ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( [], [] ); + })); + + it ( 'should detect "unlink" when removing a file and creating a directory of the same name', withContext ( async t => { + const dir = 'home/a'; + const file = 'home/a/file1'; + t.context.watchForFiles ( dir, { debounce: 300, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.remove ( file ); + t.context.tree.newDir ( file ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['unlink'], [file] ); + })); + + it ( 'should detect "add" when removing a directory and creating a file of the same name', withContext ( async t => { + const dir = 'home'; + const file = 'home/a'; + t.context.watchForFiles ( dir, { debounce: 300, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.remove ( file ); + t.context.tree.newFile ( file ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['add'], [file] ); + })); + + it ( 'should detect "change" when replacing a parent directory with another one of the same name', withContext ( async t => { + const dir = 'home'; + t.context.watchForFiles ( dir, { debounce: 300, ignoreInitial: true, recursive: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.remove ( 'home/a' ); + t.context.tree.copy ( 'home/b', 'home/a' ); + await t.context.wait.longtime (); + t.context.deepEqualUnorderedResults ( ['change', 'change'], ['home/a/file1', 'home/a/file2'] ); + })); + + it ( 'should be able to handle many "unlink" events', withContext ( async t => { + const dir = 'home/a'; + const files = t.context.tree.newFiles ( dir, 100 ); + t.context.watchForFiles ( dir, { debounce: 0, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + await t.context.wait.longtime () + files.forEach ( file => t.context.tree.remove ( file ) ); + await t.context.wait.longtime () + t.is ( t.context.events.length, 100 ); + })); + + }); + + describe ( 'directory events', it => { + + it ( 'should detect initial "addDir" for a single directory', withContext ( async t => { + const dir = 'home/a'; + t.context.watchForDirs ( dir, { debounce: 0 } ); + await t.context.wait.ready (); + await t.context.wait.time (); + t.context.deepEqualResults ( ['addDir'], [dir] ); + })); + + it ( 'should detect initial "addDir" for multiple directories', withContext ( async t => { + const dir1 = 'home/a'; + const dir2 = 'home/b'; + t.context.watchForDirs ( [dir1, dir2], { debounce: 0 } ); + await t.context.wait.ready (); + await t.context.wait.time (); + t.context.deepEqualUnorderedResults ( ['addDir', 'addDir'], [dir1, dir2] ); + })); + + it ( 'should detect initial "addDir" for directories inside a directory', withContext ( async t => { + const dir = 'home/e'; + t.context.watchForDirs ( dir, { debounce: 0 } ); + await t.context.wait.ready (); + await t.context.wait.time (); + t.context.deepEqualUnorderedResults ( ['addDir', 'addDir'], [dir, 'home/e/sub'] ); + })); + + it ( 'should detect initial "addDir" for directories inside a deep directory', withContext ( async t => { + const dir = 'home/shallow'; + t.context.watchForDirs ( dir, { debounce: 0, recursive: true } ); + await t.context.wait.ready (); + await t.context.wait.time (); + t.context.deepEqualUnorderedResults ( ['addDir', 'addDir', 'addDir'], [dir, 'home/shallow/1', 'home/shallow/1/2'] ); + })); + + it ( 'should detect "addDir" when creating a new directory inside a directory', withContext ( async t => { + const dir = 'home/a'; + const newdir = 'home/a/dir' + Math.random (); + t.context.watchForDirs ( dir, { debounce: 0, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.tree.newDir ( newdir ); + await t.context.wait.time (); + t.context.deepEqualResults ( ['addDir'], [newdir] ); + })); + + it ( 'should detect "addDir" when creating a new directory inside a new directory', withContext ( async t => { + const dir = 'home/a'; + const newdir1 = 'home/a/dir' + Math.random (); + const newdir2 = newdir1 + '/dir' + Math.random (); + t.context.watchForDirs ( dir, { debounce: 0, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.tree.newDir ( newdir2 ); + await t.context.wait.time (); + t.context.deepEqualUnorderedResults ( ['addDir', 'addDir'], [newdir1, newdir2] ); + })); + + it ( 'should detect "addDir" when creating a new directory inside a new deep directory', withContext ( async t => { + const dir = 'home'; + const newdir1 = 'home/a/dir' + Math.random (); + const newdir2 = newdir1 + '/dir' + Math.random (); + t.context.watchForDirs ( dir, { debounce: 0, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.tree.newDir ( newdir2 ); + await t.context.wait.time (); + t.context.deepEqualUnorderedResults ( ['addDir', 'addDir'], [newdir1, newdir2] ); + })); + + it ( 'should detect "addDir" when copying a directory inside a directory', withContext ( async t => { + const home = 'home/e'; + const dir = 'home/e/sub'; + const copydir = dir + Math.random (); + t.context.watchForDirs ( home, { debounce: 0, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.tree.copy ( dir, copydir ); + await t.context.wait.time (); + t.context.deepEqualResults ( ['addDir'], [copydir] ); + })); + + it ( 'should detect "addDir" when copying a directory inside a deep directory', withContext ( async t => { + const home = 'home'; + const dir = 'home/e/sub'; + const copydir = dir + Math.random (); + t.context.watchForDirs ( home, { debounce: 0, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.tree.copy ( dir, copydir ); + await t.context.wait.time (); + t.context.deepEqualResults ( ['addDir'], [copydir] ); + })); + + it ( 'should detect "addDir" when copying a parent directory', withContext ( async t => { + const home = 'home/e'; + const dir = 'home/e/sub'; + const copydir = dir + Math.random (); + t.context.watchForDirs ( home, { debounce: 0, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.tree.copy ( dir, copydir ); + await t.context.wait.time (); + t.context.deepEqualResults ( ['addDir'], [copydir] ); + })); + + it ( 'should detect "addDir" when copying a deep parent directory', withContext ( async t => { + const home = 'home'; + const dir = 'home/e'; + const copydir = dir + Math.random (); + const copysubdir = copydir + '/sub'; + t.context.watchForDirs ( home, { debounce: 0, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.tree.copy ( dir, copydir ); + await t.context.wait.time (); + t.context.deepEqualUnorderedResults ( ['addDir', 'addDir'], [copydir, copysubdir] ); + })); + + it ( 'should detect "unlinkDir" when removing a single directory', withContext ( async t => { + const dir = 'home/a'; + t.context.watchForDirs ( dir, { debounce: 0, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.hasWatchObjects ( 0, 0, 3 ); + t.context.tree.remove ( dir ); + await t.context.wait.time (); + t.context.hasWatchObjects ( 0, 1, 1 ); + t.context.deepEqualResults ( ['unlinkDir'], [dir] ); + })); + + it ( 'should detect "unlinkDir" and "addDir" when removing a single directory and much later recreating it', withContext ( async t => { + const dir = 'home/a'; + t.context.watchForDirs ( dir, { debounce: 0, ignoreInitial: true, pollingInterval: 100 } ); + await t.context.wait.ready (); + t.context.hasWatchObjects ( 0, 0, 3 ); + t.context.tree.remove ( dir ); + await t.context.wait.time (); + t.context.hasWatchObjects ( 0, 1, 1 ); + t.context.deepEqualResults ( ['unlinkDir'], [dir] ); + t.context.tree.newDir ( dir ); + await t.context.wait.time (); + t.context.hasWatchObjects ( 0, 0, 3 ); + t.context.deepEqualResults ( ['addDir'], [dir] ); + })); + + it ( 'should detect "unlinkDir" when removing a directory inside a directory', withContext ( async t => { + const dir = 'home/e'; + const subdir = 'home/e/sub'; + t.context.watchForDirs ( dir, { debounce: 0, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.tree.remove ( subdir ); + await t.context.wait.time (); + t.context.deepEqualResults ( ['unlinkDir'], [subdir] ); + })); + + it ( 'should detect "unlinkDir" when removing a directory inside a deep directory', withContext ( async t => { + const dir = 'home'; + const subdir = 'home/e/sub'; + t.context.watchForDirs ( dir, { debounce: 0, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.tree.remove ( subdir ); + await t.context.wait.time (); + t.context.deepEqualResults ( ['unlinkDir'], [subdir] ); + })); + + it ( 'should detect "unlinkDir" when removing a parent directory', withContext ( async t => { + const dir = 'home'; + t.context.watchForDirs ( dir, { debounce: 0, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.tree.remove ( 'home/e' ); + await t.context.wait.time (); + t.context.deepEqualUnorderedResults ( ['unlinkDir', 'unlinkDir'], ['home/e/sub', 'home/e'] ); + })); + + it ( 'should detect "unlinkDir" when removing a parent directory of the watcher', withContext ( async t => { + const dir = 'home/e/sub'; + t.context.watchForDirs ( dir, { debounce: 0, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.hasWatchObjects ( 0, 0, 3 ); + t.context.tree.remove ( 'home/e' ); + await t.context.wait.time (); + t.context.hasWatchObjects ( 1, 0, 0 ); + t.context.deepEqualResults ( ['unlinkDir'], [dir] ); + })); + + it ( 'should detect "unlinkDir" and "addDir" when renaming a directory inside a directory', withContext ( async t => { + const dir = 'home'; + const dir1 = 'home/a'; + const dir1alt = 'home/a_alt'; + t.context.watchForDirs ( dir, { debounce: 300, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.tree.rename ( dir1, dir1alt ); + await t.context.wait.time (); + t.context.deepEqualUnorderedResults ( ['unlinkDir', 'addDir'], [dir1, dir1alt] ); + })); + + it ( 'should detect "unlinkDir" and "addDir" when renaming a directory inside a deep directory', withContext ( async t => { + const dir = 'home'; + const dir1 = 'home/e/sub'; + const dir1alt = 'home/e/sub_alt'; + t.context.watchForDirs ( dir, { debounce: 300, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.tree.rename ( dir1, dir1alt ); + await t.context.wait.time (); + t.context.deepEqualUnorderedResults ( ['unlinkDir', 'addDir'], [dir1, dir1alt] ); + })); + + it ( 'should detect "unlinkDir" and "addDir" when renaming a parent directory', withContext ( async t => { + const dir = 'home'; + const dir1 = 'home/e'; + const dir1alt = 'home/e_alt'; + const subdir1 = 'home/e/sub'; + const subdir1alt = 'home/e_alt/sub'; + t.context.watchForDirs ( dir, { debounce: 0, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.tree.rename ( dir1, dir1alt ); + await t.context.wait.time (); + t.context.deepEqualUnorderedResults ( ['unlinkDir', 'addDir', 'unlinkDir', 'addDir'], [dir1, dir1alt, subdir1, subdir1alt] ); + })); + + it ( 'should detect nothing when creating a new directory and removing it', withContext ( async t => { + const dir = 'home/a'; + const newdir = 'home/a/dir' + Math.random (); + t.context.watchForDirs ( dir, { debounce: 0, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.tree.newDir ( newdir ); + t.context.tree.remove ( newdir ); + await t.context.wait.time (); + t.context.deepEqualResults ( [], [] ); + })); + + it ( 'should detect nothing when renaming a parent directory and rerenaming it', withContext ( async t => { + const dir = 'home'; + const dir1 = 'home/a'; + const dir1alt = 'home/a_alt'; + t.context.watchForDirs ( dir, { debounce: 0, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.tree.rename ( dir1, dir1alt ); + t.context.tree.rename ( dir1alt, dir1 ); + await t.context.wait.time (); + t.context.deepEqualResults ( [], [] ); + })); + + it ( 'should detect "addDir" when removing a file and creating a directory of the same name', withContext ( async t => { + const dir = 'home/a'; + const file = 'home/a/file1'; + t.context.watchForDirs ( dir, { debounce: 300, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.tree.remove ( file ); + t.context.tree.newDir ( file ); + await t.context.wait.time (); + t.context.deepEqualResults ( ['addDir'], [file] ); + })); + + it ( 'should detect "unlinkDir" when removing a directory and creating a file of the same name', withContext ( async t => { + const dir = 'home'; + const file = 'home/a'; + t.context.watchForDirs ( dir, { debounce: 300, ignoreInitial: true } ); + await t.context.wait.ready (); + t.context.tree.remove ( file ); + t.context.tree.newFile ( file ); + await t.context.wait.time (); + t.context.deepEqualResults ( ['unlinkDir'], [file] ); + })); + + it ( 'should detect "unlinkDir" and "addDir" when replacing a parent directory with another one of the same name', withContext ( async t => { + const dir = 'home'; + t.context.watchForDirs ( dir, { debounce: 300, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.tree.remove ( 'home/a' ); + t.context.tree.copy ( 'home/b', 'home/a' ); + await t.context.wait.time (); + t.context.deepEqualResults ( ['unlinkDir', 'addDir'], ['home/a', 'home/a'] ); + })); + + it ( 'should be able to handle many "unlinkDir" events', withContext ( async t => { + const dir = 'home/a'; + const dirs = t.context.tree.newDirs ( dir, 100 ); + t.context.watchForDirs ( dir, { debounce: 0, ignoreInitial: true } ); + await t.context.wait.ready (); + await t.context.wait.longtime () + dirs.forEach ( dir => t.context.tree.remove ( dir ) ); + await t.context.wait.longtime () + t.is ( t.context.events.length, 100 ); + })); + + }); + + describe ( 'directory events (with renames)', it => { + + it ( 'should detect initial "addDir" for a single directory', withContext ( async t => { + const dir = 'home/a'; + t.context.watchForDirs ( dir, { debounce: 0, renameDetection: true } ); + await t.context.wait.ready (); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['addDir'], [dir] ); + })); + + it ( 'should detect initial "addDir" for multiple directories', withContext ( async t => { + const dir1 = 'home/a'; + const dir2 = 'home/b'; + t.context.watchForDirs ( [dir1, dir2], { debounce: 0, renameDetection: true } ); + await t.context.wait.ready (); + await t.context.wait.longtime (); + t.context.deepEqualUnorderedResults ( ['addDir', 'addDir'], [dir1, dir2] ); + })); + + it ( 'should detect initial "addDir" for directories inside a directory', withContext ( async t => { + const dir = 'home/e'; + t.context.watchForDirs ( dir, { debounce: 0, renameDetection: true } ); + await t.context.wait.ready (); + await t.context.wait.longtime (); + t.context.deepEqualUnorderedResults ( ['addDir', 'addDir'], [dir, 'home/e/sub'] ); + })); + + it ( 'should detect initial "addDir" for directories inside a deep directory', withContext ( async t => { + const dir = 'home/shallow'; + t.context.watchForDirs ( dir, { debounce: 0, recursive: true, renameDetection: true } ); + await t.context.wait.ready (); + await t.context.wait.longtime (); + t.context.deepEqualUnorderedResults ( ['addDir', 'addDir', 'addDir'], [dir, 'home/shallow/1', 'home/shallow/1/2'] ); + })); + + it ( 'should detect "addDir" when creating a new directory inside a directory', withContext ( async t => { + const dir = 'home/a'; + const newdir = 'home/a/dir' + Math.random (); + t.context.watchForDirs ( dir, { debounce: 0, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.newDir ( newdir ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['addDir'], [newdir] ); + })); + + it ( 'should detect "addDir" when creating a new directory inside a new directory', withContext ( async t => { + const dir = 'home/a'; + const newdir1 = 'home/a/dir' + Math.random (); + const newdir2 = newdir1 + '/dir' + Math.random (); + t.context.watchForDirs ( dir, { debounce: 0, ignoreInitial: true, recursive: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.newDir ( newdir2 ); + await t.context.wait.longtime (); + t.context.deepEqualUnorderedResults ( ['addDir', 'addDir'], [newdir1, newdir2] ); + })); + + it ( 'should detect "addDir" when creating a new directory inside a new deep directory', withContext ( async t => { + const dir = 'home'; + const newdir1 = 'home/a/dir' + Math.random (); + const newdir2 = newdir1 + '/dir' + Math.random (); + t.context.watchForDirs ( dir, { debounce: 0, ignoreInitial: true, recursive: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.newDir ( newdir2 ); + await t.context.wait.longtime (); + t.context.deepEqualUnorderedResults ( ['addDir', 'addDir'], [newdir1, newdir2] ); + })); + + it ( 'should detect "addDir" when copying a directory inside a directory', withContext ( async t => { + const home = 'home/e'; + const dir = 'home/e/sub'; + const copydir = dir + Math.random (); + t.context.watchForDirs ( home, { debounce: 0, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.copy ( dir, copydir ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['addDir'], [copydir] ); + })); + + it ( 'should detect "addDir" when copying a directory inside a deep directory', withContext ( async t => { + const home = 'home'; + const dir = 'home/e/sub'; + const copydir = dir + Math.random (); + t.context.watchForDirs ( home, { debounce: 0, ignoreInitial: true, recursive: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.copy ( dir, copydir ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['addDir'], [copydir] ); + })); + + it ( 'should detect "addDir" when copying a parent directory', withContext ( async t => { + const home = 'home/e'; + const dir = 'home/e/sub'; + const copydir = dir + Math.random (); + t.context.watchForDirs ( home, { debounce: 0, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.copy ( dir, copydir ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['addDir'], [copydir] ); + })); + + it ( 'should detect "addDir" when copying a deep parent directory', withContext ( async t => { + const home = 'home'; + const dir = 'home/e'; + const copydir = dir + Math.random (); + const copysubdir = copydir + '/sub'; + t.context.watchForDirs ( home, { debounce: 0, ignoreInitial: true, recursive: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.copy ( dir, copydir ); + await t.context.wait.longtime (); + t.context.deepEqualUnorderedResults ( ['addDir', 'addDir'], [copydir, copysubdir] ); + })); + + it ( 'should detect "unlinkDir" when removing a single directory', withContext ( async t => { + const dir = 'home/a'; + t.context.watchForDirs ( dir, { debounce: 0, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.hasWatchObjects ( 0, 0, 3 ); + t.context.tree.remove ( dir ); + await t.context.wait.longtime (); + t.context.hasWatchObjects ( 0, 1, 1 ); + t.context.deepEqualResults ( ['unlinkDir'], [dir] ); + })); + + it ( 'should detect "unlinkDir" and "addDir" when removing a single directory and much later recreating it', withContext ( async t => { + const dir = 'home/a'; + t.context.watchForDirs ( dir, { debounce: 0, ignoreInitial: true, pollingInterval: 100, renameDetection: true } ); + await t.context.wait.ready (); + t.context.hasWatchObjects ( 0, 0, 3 ); + t.context.tree.remove ( dir ); + await t.context.wait.longtime (); + t.context.hasWatchObjects ( 0, 1, 1 ); + t.context.deepEqualResults ( ['unlinkDir'], [dir] ); + t.context.tree.newDir ( dir ); + await t.context.wait.longlongtime (); + t.context.hasWatchObjects ( 0, 0, 3 ); + t.context.deepEqualResults ( ['addDir'], [dir] ); + })); + + it ( 'should detect "unlinkDir" when removing a directory inside a directory', withContext ( async t => { + const dir = 'home/e'; + const subdir = 'home/e/sub'; + t.context.watchForDirs ( dir, { debounce: 0, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.remove ( subdir ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['unlinkDir'], [subdir] ); + })); + + it ( 'should detect "unlinkDir" when removing a directory inside a deep directory', withContext ( async t => { + const dir = 'home'; + const subdir = 'home/e/sub'; + t.context.watchForDirs ( dir, { debounce: 0, ignoreInitial: true, recursive: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.remove ( subdir ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['unlinkDir'], [subdir] ); + })); + + it ( 'should detect "unlinkDir" when removing a parent directory', withContext ( async t => { + const dir = 'home'; + t.context.watchForDirs ( dir, { debounce: 0, ignoreInitial: true, recursive: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.remove ( 'home/e' ); + await t.context.wait.longtime (); + t.context.deepEqualUnorderedResults ( ['unlinkDir', 'unlinkDir'], ['home/e/sub', 'home/e'] ); + })); + + it ( 'should detect "unlinkDir" when removing a parent directory of the watcher', withContext ( async t => { + const dir = 'home/e/sub'; + t.context.watchForDirs ( dir, { debounce: 0, ignoreInitial: true, recursive: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.hasWatchObjects ( 0, 0, 3 ); + t.context.tree.remove ( 'home/e' ); + await t.context.wait.longtime (); + t.context.hasWatchObjects ( 1, 0, 0 ); + t.context.deepEqualResults ( ['unlinkDir'], [dir] ); + })); + + it ( 'should detect "renameDir" when renaming a directory inside a directory', withContext ( async t => { + const dir = 'home'; + const dir1 = 'home/a'; + const dir1alt = 'home/a_alt'; + t.context.watchForDirs ( dir, { debounce: 300, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.rename ( dir1, dir1alt ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['renameDir'], [[dir1, dir1alt]] ); + })); + + it ( 'should detect "renameDir" when renaming a directory inside a deep directory', withContext ( async t => { + const dir = 'home'; + const dir1 = 'home/e/sub'; + const dir1alt = 'home/e/sub_alt'; + t.context.watchForDirs ( dir, { debounce: 300, ignoreInitial: true, recursive: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.rename ( dir1, dir1alt ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['renameDir'], [[dir1, dir1alt]] ); + })); + + it ( 'should detect "renameDir" when renaming a parent directory', withContext ( async t => { + const dir = 'home'; + const dir1 = 'home/e'; + const dir1alt = 'home/e_alt'; + const subdir1 = 'home/e/sub'; + const subdir1alt = 'home/e_alt/sub'; + t.context.watchForDirs ( dir, { debounce: 0, ignoreInitial: true, recursive: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.rename ( dir1, dir1alt ); + await t.context.wait.longlongtime (); + t.context.deepEqualUnorderedResults ( ['renameDir', 'renameDir'], [[dir1, dir1alt], [subdir1, subdir1alt]] ); + })); + + it ( 'should detect "renameDir" when renaming a directory inside a directory case-sensitively', withContext ( async t => { + const dir = 'home'; + const dir1 = 'home/a'; + const Dir1 = 'home/A'; + t.context.watchForDirs ( dir, { debounce: 300, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.rename ( dir1, Dir1 ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['renameDir'], [[dir1, Dir1]] ); + })); + + it ( 'should detect "renameDir" when renaming a directory inside a deep directory case-sensitively', withContext ( async t => { + const dir = 'home'; + const dir1 = 'home/e/sub'; + const Dir1 = 'home/e/Sub'; + t.context.watchForDirs ( dir, { debounce: 300, ignoreInitial: true, recursive: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.rename ( dir1, Dir1 ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['renameDir'], [[dir1, Dir1]] ); + })); + + it ( 'should detect "renameDir" when renaming a parent directory case-sensitively', withContext ( async t => { + const dir = 'home'; + const dir1 = 'home/e'; + const Dir1 = 'home/E'; + const subdir1 = 'home/e/sub'; + const Subdir1 = 'home/E/sub'; + t.context.watchForDirs ( dir, { debounce: 0, ignoreInitial: true, recursive: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.rename ( dir1, Dir1 ); + await t.context.wait.longlongtime (); + t.context.deepEqualUnorderedResults ( ['renameDir', 'renameDir'], [[dir1, Dir1], [subdir1, Subdir1]] ); + })); + + it ( 'should detect nothing when creating a new directory and removing it', withContext ( async t => { + const dir = 'home/a'; + const newdir = 'home/a/dir' + Math.random (); + t.context.watchForDirs ( dir, { debounce: 0, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.newDir ( newdir ); + t.context.tree.remove ( newdir ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( [], [] ); + })); + + it ( 'should detect nothing when renaming a parent directory and rerenaming it', withContext ( async t => { + const dir = 'home'; + const dir1 = 'home/a'; + const dir1alt = 'home/a_alt'; + t.context.watchForDirs ( dir, { debounce: 0, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.rename ( dir1, dir1alt ); + t.context.tree.rename ( dir1alt, dir1 ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( [], [] ); + })); + + it ( 'should detect nothing when renaming a parent directory case-sensitively and rerenaming it', withContext ( async t => { + const dir = 'home'; + const dir1 = 'home/a'; + const Dir1 = 'home/A'; + t.context.watchForDirs ( dir, { debounce: 0, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.rename ( dir1, Dir1 ); + t.context.tree.rename ( Dir1, dir1 ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( [], [] ); + })); + + it ( 'should detect "addDir" when removing a file and creating a directory of the same name', withContext ( async t => { + const dir = 'home/a'; + const file = 'home/a/file1'; + t.context.watchForDirs ( dir, { debounce: 300, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.remove ( file ); + t.context.tree.newDir ( file ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['addDir'], [file] ); + })); + + it ( 'should detect "unlinkDir" when removing a directory and creating a file of the same name', withContext ( async t => { + const dir = 'home'; + const file = 'home/a'; + t.context.watchForDirs ( dir, { debounce: 300, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.remove ( file ); + t.context.tree.newFile ( file ); + await t.context.wait.longtime (); + t.context.deepEqualResults ( ['unlinkDir'], [file] ); + })); + + it ( 'should detect "unlinkDir" and "addDir" when replacing a parent directory with another one of the same name', withContext ( async t => { + const dir = 'home'; + t.context.watchForDirs ( dir, { debounce: 300, ignoreInitial: true, recursive: true, renameDetection: true } ); + await t.context.wait.ready (); + t.context.tree.remove ( 'home/a' ); + t.context.tree.copy ( 'home/b', 'home/a' ); + await t.context.wait.longlongtime (); + t.context.deepEqualResults ( ['unlinkDir', 'addDir'], ['home/a', 'home/a'] ); + })); + + it ( 'should be able to handle many "unlinkDir" events', withContext ( async t => { + const dir = 'home/a'; + const dirs = t.context.tree.newDirs ( dir, 100 ); + t.context.watchForDirs ( dir, { debounce: 0, ignoreInitial: true, renameDetection: true } ); + await t.context.wait.ready (); + await t.context.wait.longtime () + dirs.forEach ( dir => t.context.tree.remove ( dir ) ); + await t.context.wait.longtime () + t.is ( t.context.events.length, 100 ); + })); + + }); + + describe ( 'watcher events', it => { + + it ( 'should emit "all" alongside specific target events', withContext ( async t => { + const dir = 'home/a'; + const file = 'home/a/file1'; + const newdir = 'home/a/newdir' + Math.random (); + const newfile = newdir + '/newfile' + Math.random (); + t.context.watch ( dir, { debounce: 0, ignoreInitial: true, recursive: true } ); + const adds = []; + t.context.watcher.on ( 'add', targetPath => adds.push ( targetPath ) ); + const addDirs = []; + t.context.watcher.on ( 'addDir', targetPath => addDirs.push ( targetPath ) ); + const changes = []; + t.context.watcher.on ( 'change', targetPath => changes.push ( targetPath ) ); + const unlinks = []; + t.context.watcher.on ( 'unlink', targetPath => unlinks.push ( targetPath ) ); + const unlinkDirs = []; + t.context.watcher.on ( 'unlinkDir', targetPath => unlinkDirs.push ( targetPath ) ); + await t.context.wait.ready (); + t.context.tree.modify ( file ); + t.context.tree.newFile ( newfile ); + await t.context.wait.time (); + await t.context.deepEqualUnorderedResults ( ['change', 'addDir', 'add'], [file, newdir, newfile] ); + t.deepEqual ( adds, t.context.normalizePaths ( [newfile] ) ); + t.deepEqual ( addDirs, t.context.normalizePaths ( [newdir] ) ); + t.deepEqual ( changes, t.context.normalizePaths ( [file] ) ); + t.deepEqual ( unlinks, t.context.normalizePaths ( [] ) ); + t.deepEqual ( unlinkDirs, t.context.normalizePaths ( [] ) ); + t.context.tree.remove ( file ); + t.context.tree.remove ( newdir ); + await t.context.wait.time (); + await t.context.deepEqualUnorderedResults ( ['unlink', 'unlinkDir', 'unlink'], [file, newdir, newfile] ); + t.deepEqual ( adds, t.context.normalizePaths ( [newfile] ) ); + t.deepEqual ( addDirs, t.context.normalizePaths ( [newdir] ) ); + t.deepEqual ( changes, t.context.normalizePaths ( [file] ) ); + t.context.deepEqualUnordered ( unlinks, t.context.normalizePaths ( [file, newfile] ) ); + t.deepEqual ( unlinkDirs, t.context.normalizePaths ( [newdir] ) ); + })); + + it ( 'should emit "change" only after "ready"', withContext ( async t => { + const dir = 'home/a'; + const file = 'home/a/file1'; + t.context.watch ( dir, { debounce: 0, ignoreInitial: true, recursive: true } ); + const _ready = t.context.watcher.ready.bind ( t.context.watcher ); + t.context.watcher.ready = () => { + t.context.tree.modify ( file ); + setTimeout ( _ready, 300 ); + }; + await t.context.wait.ready (); + await t.context.wait.time (); + t.true ( t.context.watcher.isReady () ); + t.context.deepEqualResults ( ['change'], [file] ); + })); + + it ( 'should emit "close" when closing', withContext ( async t => { + const file = 'home/a/file1'; + t.context.watch ( file ); + t.context.watcher.close (); + await t.context.wait.close (); + t.pass (); + })); + + it ( 'should emit "ready" when watching nothing', withContext ( async t => { + t.context.watch ( [] ); + await t.context.wait.ready (); + t.pass (); + })); + + it ( 'should emit "ready" when watching a file', withContext ( async t => { + const file = 'home/a/file1'; + t.context.watch ( file ); + await t.context.wait.ready (); + t.pass (); + })); + + it ( 'should emit "ready" when watching a directory', withContext ( async t => { + const dir = 'home'; + t.context.watch ( dir ); + await t.context.wait.ready (); + t.pass (); + })); + + it ( 'should emit "ready" when watching a directory recursively', withContext ( async t => { + const dir = 'home'; + t.context.watch ( dir, { recursive: true } ); + await t.context.wait.ready (); + t.pass (); + })); + + it ( 'should emit "ready" when watching multiple paths recursively', withContext ( async t => { + const file = 'home/b/file1'; + const dir1 = 'home/a'; + const dir2 = 'home/b'; + const dir3 = 'home'; + t.context.watch ( [file, dir1, dir2, dir3], { recursive: true } ); + await t.context.wait.ready (); + t.pass (); + })); + + it ( 'should not emit "error" when watching a non-existent file', withContext ( async t => { + const file = 'home/missing/file1'; + t.context.watch ( file ); + t.context.watcher.on ( 'error', t.fail ); + await t.context.wait.ready (); + await t.context.wait.time (); + t.pass (); + })); + + it ( 'should not emit "error" when watching a non-existent directory', withContext ( async t => { + const dir = 'home/missing'; + t.context.watch ( dir ); + t.context.watcher.on ( 'error', t.fail ); + await t.context.wait.ready (); + await t.context.wait.time (); + t.pass (); + })); + + it ( 'should not emit "error" when watching at least one non-existent path', withContext ( async t => { + const file1 = 'home/b/file1'; + const file2 = 'home/missing/file1'; + const dir = 'home'; + t.context.watch ( [file1, file2, dir], { recursive: true } ); + t.context.watcher.on ( 'error', t.fail ); + await t.context.wait.ready (); + await t.context.wait.time (); + t.pass (); + })); + + }); + + describe ( 'watcher instance', it => { + + describe ( 'close', it => { + + it ( 'should close all watchers and stop emissions', withContext ( async t => { + const dir = 'home/a'; + const file = 'home/a/file1'; + t.context.watch ( dir, { debounce: 0 } ); + t.context.watcher.on ( 'all', t.fail ); + await t.context.wait.ready (); + t.context.hasWatchObjects ( 0, 0, 3 ); + t.context.watcher.close (); + t.context.tree.modify ( file ); + t.context.tree.modify ( file, 50 ); + t.context.tree.modify ( file, 100 ); + await t.context.wait.time (); + t.true ( t.context.watcher.isClosed () ); + t.context.hasWatchObjects ( 0, 0, 0 ); + })); + + }); + + }); + + describe ( 'watcher options', it => { + + describe ( 'debounce', it => { + + it ( 'should cause delayed emissions when set to >= 0, when "ignoreInitial" is not used', withContext ( async t => { + const dir = 'home/a'; + const file = 'home/a/file1'; + const start = Date.now (); + t.context.watch ( dir, { debounce: 300 }, it => { + if ( ( Date.now () - start ) < 300 ) { + t.fail (); + } + }); + await t.context.wait.ready (); + t.context.tree.modify ( file ); + await t.context.wait.time (); + t.pass (); + })); + + it ( 'should cause delayed emissions when set to >= 0, when "ignoreInitial" is used', withContext ( async t => { + const dir = 'home/a'; + const file = 'home/a/file1'; + const start = Date.now (); + t.context.watch ( dir, { debounce: 300, ignoreInitial: true }, it => { + if ( ( Date.now () - start ) < 300 ) { + t.fail (); + } + }); + await t.context.wait.ready (); + t.context.tree.modify ( file ); + await t.context.wait.time (); + t.pass (); + })); + + }); + + describe ( 'depth', it => { + + it ( 'should not find any children when set to 0', withContext ( async t => { + const dir = 'home/deep'; + t.context.watch ( dir, { debounce: 0, depth: 0, recursive: true } ); + await t.context.wait.ready (); + await t.context.wait.time (); + t.context.deepEqualChanges ( [dir] ); + })); + + if ( HAS_NATIVE_RECURSION ) { //FIXME: These should work also when native recursion is unavailable + + it ( 'should only find immediate children when set to 1', withContext ( async t => { + const dir = 'home/deep'; + const file = 'home/deep/1'; + t.context.watch ( dir, { debounce: 0, depth: 1, recursive: true } ); + await t.context.wait.ready (); + await t.context.wait.time (); + t.context.deepEqualUnorderedChanges ( [dir, file] ); + })); + + it ( 'should only find up-to-depth-20 children when not set', withContext ( async t => { + const dir = 'home/deep'; + t.context.watch ( dir, { debounce: 0, recursive: true } ); + await t.context.wait.ready (); + await t.context.wait.time (); + t.is ( t.context.events.length, 21 ); + })); + + } + + }); + + describe ( 'ignore', it => { + + it ( 'should ignore files', withContext ( async t => { + const dir = 'home'; + const file1 = 'home/a/file1'; + const file2 = 'home/a/file2';; + t.context.watch ( dir, { + debounce: 0, + ignoreInitial: true, + recursive: true, + ignore: name => /file1/.test ( name ) + }); + await t.context.wait.ready (); + t.context.tree.modify ( file1 ); + t.context.tree.modify ( file2, 50 ); + await t.context.wait.time (); + t.context.deepEqualResults ( ['change'], [file2] ); + })); + + it ( 'should ignore directories', withContext ( async t => { + const dir = 'home'; + const file1 = 'home/e/file1'; + const file2 = 'home/e/sub/file1'; + t.context.watch ( dir, { + debounce: 0, + ignoreInitial: true, + recursive: true, + ignore: name => /sub/.test ( name ) + }); + await t.context.wait.ready (); + t.context.tree.modify ( file1 ); + t.context.tree.modify ( file2 ); + await t.context.wait.time (); + t.context.deepEqualResults ( ['change'], [file1] ); + })); + + it ( 'should ignore initial events from ignored files', withContext ( async t => { + const dir = 'home/shallow'; + t.context.watch ( dir, { + debounce: 0, + recursive: true, + ignore: name => /1\/2|1\\2/.test ( name ) + }); + await t.context.wait.ready (); + await t.context.wait.time (); + t.context.deepEqualUnorderedResults ( ['addDir', 'addDir'], [dir, 'home/shallow/1'] ); + })); + + }); + + describe ( 'ignoreInitial', it => { + + it ( 'should not emit "add" and "addDir" events when set to "true"', withContext ( async t => { + const dir = 'home/a'; + const file = 'home/b/file1'; + t.context.watch ( [dir, file], { debounce: 0, ignoreInitial: true } ); + await t.context.wait.ready (); + await t.context.wait.time (); + t.context.deepEqualResults ( [], [] ); + })); + + it ( 'should emit "add" and "addDir" events when set to "false"', withContext ( async t => { + const dir = 'home/a'; + const file = 'home/b/file1'; + t.context.watch ( [dir, file], { debounce: 0, ignoreInitial: false } ); + await t.context.wait.ready (); + await t.context.wait.time (); + t.context.deepEqualUnorderedResults ( ['addDir', 'add', 'add', 'add'], [dir, 'home/a/file1', 'home/a/file2', file] ); + })); + + it ( 'should emit "add" and "addDir" events when not set', withContext ( async t => { + const dir = 'home/a'; + const file = 'home/b/file1'; + t.context.watch ( [dir, file], { debounce: 0 } ); + await t.context.wait.ready (); + await t.context.wait.time (); + t.context.deepEqualUnorderedResults ( ['addDir', 'add', 'add', 'add'], [dir, 'home/a/file1', 'home/a/file2', file] ); + })); + + }); + + describe ( 'native', it => { + + it ( 'should only find immediate children with "depth" set to 1, when set to "false"', withContext ( async t => { + const dir = 'home/deep'; + const file = 'home/deep/1'; + t.context.watch ( dir, { debounce: 0, depth: 1, native: false, recursive: true } ); + await t.context.wait.ready (); + await t.context.wait.time (); + t.context.deepEqualUnorderedChanges ( [dir, file] ); + })); + + it ( 'should only find up-to-depth-20 children with "depth" not set, when set to "false"', withContext ( async t => { + const dir = 'home/deep'; + t.context.watch ( dir, { debounce: 0, native: false, recursive: true } ); + await t.context.wait.ready (); + await t.context.wait.time (); + t.is ( t.context.events.length, 21 ); + })); + + }); + + describe ( 'recursive', it => { + + it ( 'should not watch recursively when not set', withContext ( async t => { + const dir = 'home'; + t.context.watch ( dir, { debounce: 0, ignoreInitial: true }, t.fail ); + await t.context.wait.ready (); + t.context.tree.modify ( 'home/a/file1' ); + await t.context.wait.time (); + t.pass (); + })); + + it ( 'should not watch recursively when set to "false"', withContext ( async t => { + const dir = 'home'; + t.context.watch ( dir, { debounce: 0, ignoreInitial: true, recursive: false }, t.fail ); + await t.context.wait.ready (); + t.context.tree.modify ( 'home/a/file1' ); + await t.context.wait.time (); + t.pass (); + })); + + it ( 'should watch recursively when set to "true"', withContext ( async t => { + const dir = 'home'; + const file = 'home/a/file1'; + t.context.watchForFiles ( dir, { debounce: 0, ignoreInitial: true, recursive: true } ); + await t.context.wait.ready (); + t.context.tree.modify ( 'home/a/file1' ); + await t.context.wait.time (); + t.context.deepEqualResults ( ['change'], [file] ); + })); + + }); + + }); + +}); diff --git a/test/tree.js b/test/tree.js new file mode 100644 index 0000000..c8bf0f3 --- /dev/null +++ b/test/tree.js @@ -0,0 +1,112 @@ + +/* IMPORT */ + +import fs from 'node:fs'; +import path, { dirname } from 'node:path'; +import process from 'node:process'; + +/* MAIN */ + +class Tree { + + static ROOT = path.join ( process.cwd (), 'test', '__TREES__' ); + + static BLUEPRINT = [ + 'home/a/file1', + 'home/a/file2', + 'home/b/file1', + 'home/b/file2', + 'home/e/sub/file1', + 'home/e/file1', + 'home/e/file2', + 'home/shallow/1/2/file1', + 'home/shallow/1/2/file2', + 'home/deep/1/2/3/4/5/6/7/8/9/10/11/12/13/14/15/16/17/18/19/20/21/22/23/file1', + 'home/deep/1/2/3/4/5/6/7/8/9/10/11/12/13/14/15/16/17/18/19/20/21/22/23/file2', + 'home/empty/' + ]; + + constructor ( id ) { + this.root = path.join ( Tree.ROOT, String ( id ) ); + } + + build () { + Tree.BLUEPRINT.forEach ( path => { + if ( path.endsWith ( '/' ) ) { + fs.mkdirSync ( this.path ( path ), { recursive: true } ); + } else { + fs.mkdirSync ( dirname ( this.path ( path ) ), { recursive: true } ); + fs.writeFileSync ( this.path ( path ), '' ); + } + }); + } + + copy ( path1, path2, delay = 0 ) { + setTimeout ( () => { + fs.cpSync ( this.path ( path1 ), this.path ( path2 ), { recursive: true } ); + }, delay ); + } + + modify ( path, delay = 0 ) { + setTimeout ( () => { + fs.appendFileSync ( this.path ( path ), 'content' ); + }, delay ); + } + + newDir ( path, delay = 0 ) { + setTimeout ( () => { + fs.mkdirSync ( this.path ( path ), { recursive: true } ); + }, delay ); + } + + newDirs ( path, count ) { + return Array ( count ).fill ().map ( ( _, nr ) => { + const id = 'newdir_' + nr; + const dpath = this.path ( path, id ); + fs.mkdirSync ( dpath, { recursive: true } ); + return dpath; + }); + } + + newFile ( path, delay = 0 ) { + setTimeout ( () => { + fs.mkdirSync ( dirname ( this.path ( path ) ), { recursive: true } ); + fs.writeFileSync ( this.path ( path ), '' ); + }, delay ); + } + + newFiles ( path, count ) { + return Array ( count ).fill ().map ( ( _, nr ) => { + const id = 'newfile_' + nr; + const fpath = this.path ( path, id ); + fs.mkdirSync ( dirname ( fpath ), { recursive: true } ); + fs.writeFileSync ( fpath, '' ); + return fpath; + }); + } + + path ( ...paths ) { + if ( paths[0].startsWith ( 'home' ) ) { + return path.join ( this.root, ...paths ).replace ( /\/$/, '' ); + } else { + return path.join ( ...paths ).replace ( /\/$/, '' ); + } + } + + remove ( path, delay = 0 ) { + setTimeout ( () => { + fs.rmSync ( this.path ( path ), { recursive: true } ); + }, delay ); + } + + rename ( path1, path2, delay = 0 ) { + setTimeout ( () => { + fs.renameSync ( this.path ( path1 ), this.path ( path2 ) ); + }, delay ); + } + +} + +/* EXPORT */ + +export default Tree; diff --git a/ts/constants.ts b/ts/constants.ts new file mode 100644 index 0000000..c3a1804 --- /dev/null +++ b/ts/constants.ts @@ -0,0 +1,32 @@ + +/* IMPORT */ + +import os from 'node:os'; + +/* MAIN */ + +const DEBOUNCE = 300; + +const DEPTH = 20; + +const LIMIT = 10_000_000; + +const PLATFORM = os.platform (); + +const IS_LINUX = ( PLATFORM === 'linux' ); + +const IS_MAC = ( PLATFORM === 'darwin' ); + +const IS_WINDOWS = ( PLATFORM === 'win32' ); + +const HAS_NATIVE_RECURSION = IS_MAC || IS_WINDOWS; + +const POLLING_INTERVAL = 3000; + +const POLLING_TIMEOUT = 20000; + +const RENAME_TIMEOUT = 1250; + +/* EXPORT */ + +export {DEBOUNCE, DEPTH, LIMIT, HAS_NATIVE_RECURSION, IS_LINUX, IS_MAC, IS_WINDOWS, PLATFORM, POLLING_INTERVAL, POLLING_TIMEOUT, RENAME_TIMEOUT}; diff --git a/ts/dettle/debounce.ts b/ts/dettle/debounce.ts new file mode 100644 index 0000000..9c171ee --- /dev/null +++ b/ts/dettle/debounce.ts @@ -0,0 +1,152 @@ + +/* IMPORT */ + +import type {FN, Debounced} from './types.js'; + +/* MAIN */ + +const debounce = ( fn: FN, wait: number = 1, options?: { leading?: boolean, trailing?: boolean, maxWait?: number } ): Debounced => { + + /* VARIABLES */ + + wait = Math.max ( 1, wait ); + + const leading = options?.leading ?? false; + const trailing = options?.trailing ?? true; + const maxWait = Math.max ( options?.maxWait ?? Infinity, wait ); + + let args: Args | undefined; + let timeout: ReturnType | undefined; + let timestampCall = 0; + let timestampInvoke = 0; + + /* HELPERS */ + + const getInstantData = (): [number, boolean] => { + + const timestamp = Date.now (); + const elapsedCall = timestamp - timestampCall; + const elapsedInvoke = timestamp - timestampInvoke; + const isInvoke = ( elapsedCall >= wait || elapsedInvoke >= maxWait ); + + return [timestamp, isInvoke]; + + }; + + const invoke = ( timestamp: number ): void => { + + timestampInvoke = timestamp; + + if ( !args ) return; // This should never happen + + const _args = args; + + args = undefined; + + fn.apply ( undefined, _args ); + + }; + + const onCancel = (): void => { + + resetTimeout ( 0 ); + + }; + + const onFlush = (): void => { + + if ( !timeout ) return; + + onCancel (); + + invoke ( Date.now () ); + + }; + + const onLeading = ( timestamp: number ): void => { + + timestampInvoke = timestamp; + + if ( leading ) return invoke ( timestamp ); + + }; + + const onTrailing = ( timestamp: number ): void => { + + if ( trailing && args ) return invoke ( timestamp ); + + args = undefined; + + }; + + const onTimeout = (): void => { + + timeout = undefined; + + const [timestamp, isInvoking] = getInstantData (); + + if ( isInvoking ) return onTrailing ( timestamp ); + + return updateTimeout ( timestamp ); + + }; + + const updateTimeout = ( timestamp: number ): void => { + + const elapsedCall = timestamp - timestampCall; + const elapsedInvoke = timestamp - timestampInvoke; + const remainingCall = wait - elapsedCall; + const remainingInvoke = maxWait - elapsedInvoke; + const ms = Math.min ( remainingCall, remainingInvoke ); + + return resetTimeout ( ms ); + + }; + + const resetTimeout = ( ms: number ): void => { + + if ( timeout ) clearTimeout ( timeout ); + + if ( ms <= 0 ) return; + + timeout = setTimeout ( onTimeout, ms ); + + }; + + /* DEBOUNCED */ + + const debounced = ( ...argsLatest: Args ): void => { + + const [timestamp, isInvoking] = getInstantData (); + const hadTimeout = !!timeout; + + args = argsLatest; + timestampCall = timestamp; + + if ( isInvoking || !timeout ) resetTimeout ( wait ); + + if ( isInvoking ) { + + if ( !hadTimeout ) return onLeading ( timestamp ); + + return invoke ( timestamp ); + + } + + }; + + /* DEBOUNCED UTILITIES */ + + debounced.cancel = onCancel; + + debounced.flush = onFlush; + + /* RETURN */ + + return debounced; + +}; + +/* EXPORT */ + +export default debounce; diff --git a/ts/dettle/index.ts b/ts/dettle/index.ts new file mode 100755 index 0000000..6958157 --- /dev/null +++ b/ts/dettle/index.ts @@ -0,0 +1,9 @@ + +/* IMPORT */ + +import debounce from './debounce.js'; +import throttle from './throttle.js'; + +/* EXPORT */ + +export {debounce, throttle}; diff --git a/ts/dettle/license b/ts/dettle/license new file mode 100644 index 0000000..fe9baaa --- /dev/null +++ b/ts/dettle/license @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2023-present Fabio Spampinato + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/ts/dettle/throttle.ts b/ts/dettle/throttle.ts new file mode 100644 index 0000000..223586a --- /dev/null +++ b/ts/dettle/throttle.ts @@ -0,0 +1,21 @@ + +/* IMPORT */ + +import debounce from './debounce.js'; +import type {FN, Throttled} from './types.js'; + +/* MAIN */ + +const throttle = ( fn: FN, wait: number = 1, options?: { leading?: boolean, trailing?: boolean } ): Throttled => { + + return debounce ( fn, wait, { + maxWait: wait, + leading: options?.leading ?? true, + trailing: options?.trailing ?? true + }); + +}; + +/* EXPORT */ + +export default throttle; diff --git a/ts/dettle/types.ts b/ts/dettle/types.ts new file mode 100644 index 0000000..526207a --- /dev/null +++ b/ts/dettle/types.ts @@ -0,0 +1,14 @@ + +/* MAIN */ + +type Callback = () => void; + +type FN = ( ...args: Args ) => Return; + +type Debounced = FN & { cancel: Callback, flush: Callback }; + +type Throttled = FN & { cancel: Callback, flush: Callback }; + +/* EXPORT */ + +export type {Callback, FN, Debounced, Throttled}; diff --git a/ts/enums.ts b/ts/enums.ts new file mode 100644 index 0000000..a094f1d --- /dev/null +++ b/ts/enums.ts @@ -0,0 +1,38 @@ + +/* MAIN */ + +const enum FileType { + DIR = 1, + FILE = 2 +} + +const enum FSTargetEvent { + CHANGE = 'change', + RENAME = 'rename' +} + +const enum FSWatcherEvent { + CHANGE = 'change', + ERROR = 'error' +} + +const enum TargetEvent { + ADD = 'add', + ADD_DIR = 'addDir', + CHANGE = 'change', + RENAME = 'rename', + RENAME_DIR = 'renameDir', + UNLINK = 'unlink', + UNLINK_DIR = 'unlinkDir' +} + +const enum WatcherEvent { + ALL = 'all', + CLOSE = 'close', + ERROR = 'error', + READY = 'ready' +} + +/* EXPORT */ + +export {FileType, FSTargetEvent, FSWatcherEvent, TargetEvent, WatcherEvent}; diff --git a/ts/lazy_map_set.ts b/ts/lazy_map_set.ts new file mode 100644 index 0000000..64b93e2 --- /dev/null +++ b/ts/lazy_map_set.ts @@ -0,0 +1,144 @@ + +/* IMPORT */ + +import Utils from './utils.js'; + +/* MAIN */ + +//TODO: Maybe publish this as a standalone module + +class LazyMapSet { + + /* VARIABLES */ + + private map: Map | V> = new Map (); + + /* API */ + + clear (): void { + + this.map.clear (); + + } + + delete ( key: K, value?: V ): boolean { + + if ( Utils.lang.isUndefined ( value ) ) { + + return this.map.delete ( key ); + + } else if ( this.map.has ( key ) ) { + + const values = this.map.get ( key ); + + if ( Utils.lang.isSet ( values ) ) { + + const deleted = values.delete ( value ); + + if ( !values.size ) { + + this.map.delete ( key ); + + } + + return deleted; + + } else if ( values === value ) { + + this.map.delete ( key ); + + return true; + + } + + } + + return false; + + } + + find ( key: K, iterator: ( value: V ) => boolean ): V | undefined { + + if ( this.map.has ( key ) ) { + + const values = this.map.get ( key ); + + if ( Utils.lang.isSet ( values ) ) { + + return Array.from ( values ).find ( iterator ); + + } else if ( iterator ( values! ) ) { //TSC + + return values; + + } + + } + + return undefined; + + } + + get ( key: K ): Set | V | undefined { + + return this.map.get ( key ); + + } + + has ( key: K, value?: V ): boolean { + + if ( Utils.lang.isUndefined ( value ) ) { + + return this.map.has ( key ); + + } else if ( this.map.has ( key ) ) { + + const values = this.map.get ( key ); + + if ( Utils.lang.isSet ( values ) ) { + + return values.has ( value ); + + } else { + + return ( values === value ); + + } + + } + + return false; + + } + + set ( key: K, value: V ): this { + + if ( this.map.has ( key ) ) { + + const values = this.map.get ( key ); + + if ( Utils.lang.isSet ( values ) ) { + + values.add ( value ); + + } else if ( values !== value ) { + + this.map.set ( key, new Set ([ values!, value ]) ); //TSC + + } + + } else { + + this.map.set ( key, value ); + + } + + return this; + + } + +} + +/* EXPORT */ + +export default LazyMapSet; diff --git a/ts/promise-make-naked/constants.ts b/ts/promise-make-naked/constants.ts new file mode 100644 index 0000000..c7da0b7 --- /dev/null +++ b/ts/promise-make-naked/constants.ts @@ -0,0 +1,8 @@ + +/* MAIN */ + +const NOOP = (): void => {}; + +/* EXPORT */ + +export {NOOP}; diff --git a/ts/promise-make-naked/index.ts b/ts/promise-make-naked/index.ts new file mode 100755 index 0000000..0438d73 --- /dev/null +++ b/ts/promise-make-naked/index.ts @@ -0,0 +1,53 @@ + +/* IMPORT */ + +import {NOOP} from './constants.js'; +import type {PromiseResolve, PromiseReject, Result} from './types.js'; + +/* MAIN */ + +const makeNakedPromise = (): Result => { + + let resolve: PromiseResolve = NOOP; + let reject: PromiseReject = NOOP; + + let resolved = false; + let rejected = false; + + const promise = new Promise ( ( res, rej ): void => { + + resolve = value => { + resolved = true; + return res ( value ); + }; + + reject = value => { + rejected = true; + return rej ( value ); + }; + + }); + + const isPending = (): boolean => !resolved && !rejected; + const isResolved = (): boolean => resolved; + const isRejected = (): boolean => rejected; + + return {promise, resolve, reject, isPending, isResolved, isRejected}; + +}; + +/* UTILITIES */ + +makeNakedPromise.wrap = async ( fn: ( result: Result ) => void ): Promise => { + + const result = makeNakedPromise (); + + await fn ( result ); + + return result.promise; + +}; + +/* EXPORT */ + +export default makeNakedPromise; diff --git a/ts/promise-make-naked/license b/ts/promise-make-naked/license new file mode 100644 index 0000000..a61e17b --- /dev/null +++ b/ts/promise-make-naked/license @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2021-present Fabio Spampinato + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/ts/promise-make-naked/types.ts b/ts/promise-make-naked/types.ts new file mode 100644 index 0000000..e0491b3 --- /dev/null +++ b/ts/promise-make-naked/types.ts @@ -0,0 +1,19 @@ + +/* MAIN */ + +type PromiseResolve = ( value: T | PromiseLike ) => void; + +type PromiseReject = ( reason?: unknown ) => void; + +type Result = { + promise: Promise, + resolve: PromiseResolve, + reject: PromiseReject, + isPending: () => boolean, + isResolved: () => boolean, + isRejected: () => boolean +}; + +/* EXPORT */ + +export type {PromiseResolve, PromiseReject, Result}; diff --git a/ts/tiny-readdir/index.ts b/ts/tiny-readdir/index.ts new file mode 100755 index 0000000..e3a5a8b --- /dev/null +++ b/ts/tiny-readdir/index.ts @@ -0,0 +1,246 @@ + +/* IMPORT */ + +import fs from 'node:fs'; +import path from 'node:path'; +import {isFunction, makeCounterPromise} from './utils.js'; +import type {Options, ResultDirectory, ResultDirectories, Result} from './types.js'; + +/* MAIN */ + +//TODO: Streamline the type of dirnmaps + +const readdir = ( rootPath: string, options?: Options ): Promise => { + + const followSymlinks = options?.followSymlinks ?? false; + const maxDepth = options?.depth ?? Infinity; + const maxPaths = options?.limit ?? Infinity; + const ignore = options?.ignore ?? (() => false); + const isIgnored = isFunction ( ignore ) ? ignore : ( targetPath: string ) => ignore.test ( targetPath ); + const signal = options?.signal ?? { aborted: false }; + const directories: string[] = []; + const directoriesNames: Set = new Set (); + const directoriesNamesToPaths: Record = {}; + const files: string[] = []; + const filesNames: Set = new Set (); + const filesNamesToPaths: Record = {}; + const symlinks: string[] = []; + const symlinksNames: Set = new Set (); + const symlinksNamesToPaths: Record = {}; + const map: ResultDirectories = {}; + const visited = new Set (); + const resultEmpty: Result = { directories: [], directoriesNames: new Set (), directoriesNamesToPaths: {}, files: [], filesNames: new Set (), filesNamesToPaths: {}, symlinks: [], symlinksNames: new Set (), symlinksNamesToPaths: {}, map: {} }; + const result: Result = { directories, directoriesNames, directoriesNamesToPaths, files, filesNames, filesNamesToPaths, symlinks, symlinksNames, symlinksNamesToPaths, map }; + const {promise, increment, decrement} = makeCounterPromise (); + + let foundPaths = 0; + + const handleDirectory = ( dirmap: ResultDirectory, subPath: string, name: string, depth: number ): void => { + + if ( visited.has ( subPath ) ) return; + + if ( foundPaths >= maxPaths ) return; + + foundPaths += 1; + dirmap.directories.push ( subPath ); + dirmap.directoriesNames.add ( name ); + // dirmap.directoriesNamesToPaths.propertyIsEnumerable(name) || ( dirmap.directoriesNamesToPaths[name] = [] ); + // dirmap.directoriesNamesToPaths[name].push ( subPath ); + directories.push ( subPath ); + directoriesNames.add ( name ); + directoriesNamesToPaths.propertyIsEnumerable(name) || ( directoriesNamesToPaths[name] = [] ); + directoriesNamesToPaths[name].push ( subPath ); + visited.add ( subPath ); + + if ( depth >= maxDepth ) return; + + if ( foundPaths >= maxPaths ) return; + + populateResultFromPath ( subPath, depth + 1 ); + + }; + + const handleFile = ( dirmap: ResultDirectory, subPath: string, name: string ): void => { + + if ( visited.has ( subPath ) ) return; + + if ( foundPaths >= maxPaths ) return; + + foundPaths += 1; + dirmap.files.push ( subPath ); + dirmap.filesNames.add ( name ); + // dirmap.filesNamesToPaths.propertyIsEnumerable(name) || ( dirmap.filesNamesToPaths[name] = [] ); + // dirmap.filesNamesToPaths[name].push ( subPath ); + files.push ( subPath ); + filesNames.add ( name ); + filesNamesToPaths.propertyIsEnumerable(name) || ( filesNamesToPaths[name] = [] ); + filesNamesToPaths[name].push ( subPath ); + visited.add ( subPath ); + + }; + + const handleSymlink = ( dirmap: ResultDirectory, subPath: string, name: string, depth: number ): void => { + + if ( visited.has ( subPath ) ) return; + + if ( foundPaths >= maxPaths ) return; + + foundPaths += 1; + dirmap.symlinks.push ( subPath ); + dirmap.symlinksNames.add ( name ); + // dirmap.symlinksNamesToPaths.propertyIsEnumerable(name) || ( dirmap.symlinksNamesToPaths[name] = [] ); + // dirmap.symlinksNamesToPaths[name].push ( subPath ); + symlinks.push ( subPath ); + symlinksNames.add ( name ); + symlinksNamesToPaths.propertyIsEnumerable(name) || ( symlinksNamesToPaths[name] = [] ); + symlinksNamesToPaths[name].push ( subPath ); + visited.add ( subPath ); + + if ( !followSymlinks ) return; + + if ( depth >= maxDepth ) return; + + if ( foundPaths >= maxPaths ) return; + + populateResultFromSymlink ( subPath, depth + 1 ); + + }; + + const handleStat = ( dirmap: ResultDirectory, rootPath: string, name: string, stat: fs.Stats, depth: number ): void => { + + if ( signal.aborted ) return; + + if ( isIgnored ( rootPath ) ) return; + + if ( stat.isDirectory () ) { + + handleDirectory ( dirmap, rootPath, name, depth ); + + } else if ( stat.isFile () ) { + + handleFile ( dirmap, rootPath, name ); + + } else if ( stat.isSymbolicLink () ) { + + handleSymlink ( dirmap, rootPath, name, depth ); + + } + + }; + + const handleDirent = ( dirmap: ResultDirectory, rootPath: string, dirent: fs.Dirent, depth: number ): void => { + + if ( signal.aborted ) return; + + const separator = ( rootPath === path.sep ) ? '' : path.sep; + const name = dirent.name; + const subPath = `${rootPath}${separator}${name}`; + + if ( isIgnored ( subPath ) ) return; + + if ( dirent.isDirectory () ) { + + handleDirectory ( dirmap, subPath, name, depth ); + + } else if ( dirent.isFile () ) { + + handleFile ( dirmap, subPath, name ); + + } else if ( dirent.isSymbolicLink () ) { + + handleSymlink ( dirmap, subPath, name, depth ); + + } + + }; + + const handleDirents = ( dirmap: ResultDirectory, rootPath: string, dirents: fs.Dirent[], depth: number ): void => { + + for ( let i = 0, l = dirents.length; i < l; i++ ) { + + handleDirent ( dirmap, rootPath, dirents[i], depth ); + + } + + }; + + const populateResultFromPath = ( rootPath: string, depth: number ): void => { + + if ( signal.aborted ) return; + + if ( depth > maxDepth ) return; + + if ( foundPaths >= maxPaths ) return; + + increment (); + + fs.readdir ( rootPath, { withFileTypes: true }, ( error, dirents ) => { + + if ( error ) return decrement (); + + if ( signal.aborted ) return decrement (); + + if ( !dirents.length ) return decrement (); + + const dirmap = map[rootPath] = { directories: [], directoriesNames: new Set (), directoriesNamesToPaths: {}, files: [], filesNames: new Set (), filesNamesToPaths: {}, symlinks: [], symlinksNames: new Set (), symlinksNamesToPaths: {} }; + + handleDirents ( dirmap, rootPath, dirents, depth ); + + decrement (); + + }); + + }; + + const populateResultFromSymlink = async ( rootPath: string, depth: number ): Promise => { + + increment (); + + fs.realpath ( rootPath, ( error, realPath ) => { + + if ( error ) return decrement (); + + if ( signal.aborted ) return decrement (); + + fs.stat ( realPath, async ( error, stat ) => { + + if ( error ) return decrement (); + + if ( signal.aborted ) return decrement (); + + const name = path.basename ( realPath ); + const dirmap = map[rootPath] = { directories: [], directoriesNames: new Set (), directoriesNamesToPaths: {}, files: [], filesNames: new Set (), filesNamesToPaths: {}, symlinks: [], symlinksNames: new Set (), symlinksNamesToPaths: {} }; + + handleStat ( dirmap, realPath, name, stat, depth ); + + decrement (); + + }); + + }); + + }; + + const getResult = async ( rootPath: string, depth: number = 1 ): Promise => { + + rootPath = path.normalize ( rootPath ); + + visited.add ( rootPath ); + + populateResultFromPath ( rootPath, depth ); + + await promise; + + if ( signal.aborted ) return resultEmpty; + + return result; + + }; + + return getResult ( rootPath ); + +}; + +/* EXPORT */ + +export default readdir; diff --git a/ts/tiny-readdir/license b/ts/tiny-readdir/license new file mode 100644 index 0000000..969ded7 --- /dev/null +++ b/ts/tiny-readdir/license @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2020-present Fabio Spampinato + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/ts/tiny-readdir/types.ts b/ts/tiny-readdir/types.ts new file mode 100644 index 0000000..8cd0d66 --- /dev/null +++ b/ts/tiny-readdir/types.ts @@ -0,0 +1,38 @@ + +/* HELPERS */ + +type Callback = () => void; + +/* MAIN */ + +type Options = { + depth?: number, + limit?: number, + followSymlinks?: boolean, + ignore?: (( targetPath: string ) => boolean) | RegExp, + signal?: { aborted: boolean } +}; + +type ResultDirectory = { + directories: string[], + directoriesNames: Set, + directoriesNamesToPaths: Record, + files: string[], + filesNames: Set, + filesNamesToPaths: Record, + symlinks: string[], + symlinksNames: Set, + symlinksNamesToPaths: Record +}; + +type ResultDirectories = { + [path: string]: ResultDirectory +}; + +type Result = ResultDirectory & { + map: ResultDirectories +}; + +/* EXPORT */ + +export type {Callback, Options, ResultDirectory, ResultDirectories, Result}; diff --git a/ts/tiny-readdir/utils.ts b/ts/tiny-readdir/utils.ts new file mode 100644 index 0000000..782e434 --- /dev/null +++ b/ts/tiny-readdir/utils.ts @@ -0,0 +1,43 @@ + +/* IMPORT */ + +import makeNakedPromise from '../promise-make-naked/index.js'; +import type {Callback} from './types.js'; + +/* MAIN */ + +const isFunction = ( value: unknown ): value is Function => { + + return ( typeof value === 'function' ); + +}; + +const makeCounterPromise = (): { promise: Promise, increment: Callback, decrement: Callback } => { + + const {promise, resolve} = makeNakedPromise (); + + let counter = 0; + + const increment = (): void => { + + counter += 1; + + }; + + const decrement = (): void => { + + counter -= 1; + + if ( counter ) return; + + resolve (); + + }; + + return { promise, increment, decrement }; + +}; + +/* EXPORT */ + +export {isFunction, makeCounterPromise}; diff --git a/ts/types.ts b/ts/types.ts new file mode 100644 index 0000000..061eafc --- /dev/null +++ b/ts/types.ts @@ -0,0 +1,104 @@ + +/* IMPORT */ + +import type {FSWatcher, BigIntStats} from 'node:fs'; +import type {FSTargetEvent, TargetEvent} from './enums.js'; +import type WatcherStats from './watcher_stats.js'; + +type ResultDirectory = { + directories: string[]; + directoriesNames: Set; + directoriesNamesToPaths: Record; + files: string[]; + filesNames: Set; + filesNamesToPaths: Record; + symlinks: string[]; + symlinksNames: Set; + symlinksNamesToPaths: Record; +}; +type ResultDirectories = { + [path: string]: ResultDirectory; +}; + +/* MAIN */ + +type Callback = () => void; + +type Disposer = () => void; + +type Event = [TargetEvent, Path, Path?]; + +type FSHandler = ( event?: FSTargetEvent, targetName?: string ) => void; + +type Handler = ( event: TargetEvent, targetPath: Path, targetPathNext?: Path ) => void; + +type HandlerBatched = ( event?: FSTargetEvent, targetPath?: Path, isInitial?: boolean ) => Promise; + +type Ignore = (( targetPath: Path ) => boolean) | RegExp; + +type INO = bigint | number; + +type Path = string; + +type ReaddirMap = ResultDirectories; + +type Stats = BigIntStats; + +type LocksAdd = Map void>; + +type LocksUnlink = Map Path>; + +type LocksPair = { + add: LocksAdd, + unlink: LocksUnlink +}; + +type LockConfig = { + ino?: INO, + targetPath: Path, + locks: LocksPair, + events: { + add: TargetEvent.ADD | TargetEvent.ADD_DIR, + change?: TargetEvent.CHANGE, + rename: TargetEvent.RENAME | TargetEvent.RENAME_DIR, + unlink: TargetEvent.UNLINK | TargetEvent.UNLINK_DIR + } +}; + +type PollerConfig = { + options: WatcherOptions, + targetPath: Path +}; + +type SubwatcherConfig = { + options: WatcherOptions, + targetPath: Path +}; + +type WatcherConfig = { + handler: Handler, + watcher: FSWatcher, + options: WatcherOptions, + folderPath: Path, + filePath?: Path +}; + +type WatcherOptions = { + debounce?: number, + depth?: number, //FIXME: Not respected when events are detected and native recursion is available, but setting a maximum depth is mostly relevant for the non-native implemention + limit?: number, //FIXME: Not respected for newly added directories, but hard to keep track of everything and not has important + ignore?: Ignore, + ignoreInitial?: boolean, + native?: boolean, + persistent?: boolean, + pollingInterval?: number, + pollingTimeout?: number, + readdirMap?: ReaddirMap, + recursive?: boolean, + renameDetection?: boolean, + renameTimeout?: number //TODO: Having a timeout for these sorts of things isn't exactly reliable, but what's the better option? +}; + +/* EXPORT */ + +export type {Callback, Disposer, Event, FSHandler, FSWatcher, Handler, HandlerBatched, Ignore, INO, Path, ReaddirMap, Stats, LocksAdd, LocksUnlink, LocksPair, LockConfig, PollerConfig, SubwatcherConfig, WatcherConfig, WatcherOptions, WatcherStats}; diff --git a/ts/utils.ts b/ts/utils.ts new file mode 100644 index 0000000..66c4dcb --- /dev/null +++ b/ts/utils.ts @@ -0,0 +1,208 @@ + +/* IMPORT */ + +import { debounce } from './dettle/index.js'; +import fs from 'node:fs'; +import path from 'node:path'; +import sfs from 'stubborn-fs'; +import readdir from './tiny-readdir/index.js'; +import {POLLING_TIMEOUT} from './constants.js'; +import type {Callback, Ignore, ReaddirMap, Stats} from './types.js'; + +/* MAIN */ + +const Utils = { + + /* LANG API */ + + lang: { //TODO: Import all these utilities from "nanodash" instead + + debounce, + + attempt: ( fn: () => T ): T | Error => { + + try { + + return fn (); + + } catch ( error: unknown ) { + + return Utils.lang.castError ( error ); + + } + + }, + + castArray: ( x: T | T[] ): T[] => { + + return Utils.lang.isArray ( x ) ? x : [x]; + + }, + + castError: ( exception: unknown ): Error => { + + if ( Utils.lang.isError ( exception ) ) return exception; + + if ( Utils.lang.isString ( exception ) ) return new Error ( exception ); + + return new Error ( 'Unknown error' ); + + }, + + defer: ( callback: Callback ): NodeJS.Timeout => { + + return setTimeout ( callback, 0 ); + + }, + + isArray: ( value: unknown ): value is unknown[] => { + + return Array.isArray ( value ); + + }, + + isError: ( value: unknown ): value is Error => { + + return value instanceof Error; + + }, + + isFunction: ( value: unknown ): value is Function => { + + return typeof value === 'function'; + + }, + + isNaN: ( value: unknown ): value is number => { + + return Number.isNaN ( value ); + + }, + + isNumber: ( value: unknown ): value is number => { + + return typeof value === 'number'; + + }, + + isPrimitive: ( value: unknown ): value is bigint | symbol | string | number | boolean | null | undefined => { + + if ( value === null ) return true; + + const type = typeof value; + + return type !== 'object' && type !== 'function'; + + }, + + isShallowEqual: ( x: any, y: any ): boolean => { + + if ( x === y ) return true; + + if ( Utils.lang.isNaN ( x ) ) return Utils.lang.isNaN ( y ); + + if ( Utils.lang.isPrimitive ( x ) || Utils.lang.isPrimitive ( y ) ) return x === y; + + for ( const i in x ) if ( !( i in y ) ) return false; + + for ( const i in y ) if ( x[i] !== y[i] ) return false; + + return true; + + }, + + isSet: ( value: unknown ): value is Set => { + + return value instanceof Set; + + }, + + isString: ( value: unknown ): value is string => { + + return typeof value === 'string'; + + }, + + isUndefined: ( value: unknown ): value is undefined => { + + return value === undefined; + + }, + + noop: (): undefined => { + + return; + + }, + + uniq: ( arr: T[] ): T[] => { + + if ( arr.length < 2 ) return arr; + + return Array.from ( new Set ( arr ) ); + + } + + }, + + /* FS API */ + + fs: { + + getDepth: ( targetPath: string ): number => { + + return Math.max ( 0, targetPath.split ( path.sep ).length - 1 ); + + }, + + getRealPath: ( targetPath: string, native?: boolean ): string | undefined => { + + try { + + return native ? fs.realpathSync.native ( targetPath ) : fs.realpathSync ( targetPath ); + + } catch { + + return; + + } + + }, + + isSubPath: ( targetPath: string, subPath: string ): boolean => { + + return ( subPath.startsWith ( targetPath ) && subPath[targetPath.length] === path.sep && ( subPath.length - targetPath.length ) > path.sep.length ); + + }, + + poll: ( targetPath: string, timeout: number = POLLING_TIMEOUT ): Promise => { + + return sfs.retry.stat ( timeout )( targetPath, { bigint: true } ).catch ( Utils.lang.noop ); + + }, + + readdir: async ( rootPath: string, ignore?: Ignore, depth: number = Infinity, limit: number = Infinity, signal?: { aborted: boolean }, readdirMap?: ReaddirMap ): Promise<[string[], string[]]> => { + + if ( readdirMap && depth === 1 && rootPath in readdirMap ) { // Reusing cached data + + const result = readdirMap[rootPath]; + + return [result.directories, result.files]; + + } else { // Retrieving fresh data + + const result = await readdir ( rootPath, { depth, limit, ignore, signal } ); + + return [result.directories, result.files]; + + } + + } + + } + +}; + +/* EXPORT */ + +export default Utils; diff --git a/ts/watcher.ts b/ts/watcher.ts new file mode 100644 index 0000000..cd7223f --- /dev/null +++ b/ts/watcher.ts @@ -0,0 +1,655 @@ + +/* IMPORT */ + +import {EventEmitter} from 'node:events'; +import fs from 'node:fs'; +import path from 'node:path'; +import {DEPTH, LIMIT, HAS_NATIVE_RECURSION, POLLING_INTERVAL} from './constants.js'; +import {TargetEvent, WatcherEvent} from './enums.js'; +import Utils from './utils.js'; +import WatcherHandler from './watcher_handler.js'; +import WatcherLocker from './watcher_locker.js'; +import WatcherPoller from './watcher_poller.js'; +import type {Callback, Disposer, Handler, Ignore, Path, PollerConfig, SubwatcherConfig, WatcherOptions, WatcherConfig} from './types.js'; + +/* MAIN */ + +class Watcher extends EventEmitter { + + /* VARIABLES */ + + _closed: boolean; + _ready: boolean; + _closeAborter: AbortController; + _closeSignal: { aborted: boolean }; + _closeWait: Promise; + _readyWait: Promise; + _locker: WatcherLocker; + _roots: Set; + _poller: WatcherPoller; + _pollers: Set; + _subwatchers: Set; + _watchers: Record; + _watchersLock: Promise; + _watchersRestorable: Record; + _watchersRestoreTimeout?: NodeJS.Timeout; + + /* CONSTRUCTOR */ + + constructor ( target?: Path[] | Path | Handler, options?: WatcherOptions | Handler, handler?: Handler ) { + + super (); + + this._closed = false; + this._ready = false; + this._closeAborter = new AbortController (); + this._closeSignal = this._closeAborter.signal; + this.on ( WatcherEvent.CLOSE, () => this._closeAborter.abort () ); + this._closeWait = new Promise ( resolve => this.on ( WatcherEvent.CLOSE, resolve ) ); + this._readyWait = new Promise ( resolve => this.on ( WatcherEvent.READY, resolve ) ); + this._locker = new WatcherLocker ( this ); + this._roots = new Set (); + this._poller = new WatcherPoller (); + this._pollers = new Set (); + this._subwatchers = new Set (); + this._watchers = {}; + this._watchersLock = Promise.resolve (); + this._watchersRestorable = {}; + + this.watch ( target, options, handler ); + + } + + /* API */ + + isClosed (): boolean { + + return this._closed; + + } + + isIgnored ( targetPath: Path, ignore?: Ignore ): boolean { + + return !!ignore && ( Utils.lang.isFunction ( ignore ) ? !!ignore ( targetPath ) : ignore.test ( targetPath ) ); + + } + + isReady (): boolean { + + return this._ready; + + } + + close (): boolean { + + this._locker.reset (); + this._poller.reset (); + this._roots.clear (); + + this.watchersClose (); + + if ( this.isClosed () ) return false; + + this._closed = true; + + return this.emit ( WatcherEvent.CLOSE ); + + } + + error ( exception: unknown ): boolean { + + if ( this.isClosed () ) return false; + + const error = Utils.lang.castError ( exception ); + + return this.emit ( WatcherEvent.ERROR, error ); + + } + + event ( event: TargetEvent, targetPath: Path, targetPathNext?: Path ): boolean { + + if ( this.isClosed () ) return false; + + this.emit ( WatcherEvent.ALL, event, targetPath, targetPathNext ); + + return this.emit ( event, targetPath, targetPathNext ); + + } + + ready (): boolean { + + if ( this.isClosed () || this.isReady () ) return false; + + this._ready = true; + + return this.emit ( WatcherEvent.READY ); + + } + + pollerExists ( targetPath: Path, options: WatcherOptions ): boolean { //FIXME: This doesn't actually allow for multiple pollers to the same paths, but potentially in the future the same path could be polled with different callbacks to be called, which this doesn't currently allow for + + for ( const poller of this._pollers ) { + + if ( poller.targetPath !== targetPath ) continue; + + if ( !Utils.lang.isShallowEqual ( poller.options, options ) ) continue; + + return true; + + } + + return false; + + } + + subwatcherExists ( targetPath: Path, options: WatcherOptions ): boolean { //FIXME: This doesn't actually allow for multiple subwatchers to the same paths, but potentially in the future the same path could be subwatched with different callbacks to be called, which this doesn't currently allow for + + for ( const subwatcher of this._subwatchers ) { + + if ( subwatcher.targetPath !== targetPath ) continue; + + if ( !Utils.lang.isShallowEqual ( subwatcher.options, options ) ) continue; + + return true; + + } + + return false; + + } + + watchersClose ( folderPath?: Path, filePath?: Path, recursive: boolean = true ): void { + + if ( !folderPath ) { + + for ( const folderPath in this._watchers ) { + + this.watchersClose ( folderPath, filePath, false ); + + } + + } else { + + const configs = this._watchers[folderPath]; + + if ( configs ) { + + for ( const config of [...configs] ) { // It's important to clone the array, as items will be deleted from it also + + if ( filePath && config.filePath !== filePath ) continue; + + this.watcherClose ( config ); + + } + + } + + if ( recursive ) { + + for ( const folderPathOther in this._watchers ) { + + if ( !Utils.fs.isSubPath ( folderPath, folderPathOther ) ) continue; + + this.watchersClose ( folderPathOther, filePath, false ); + + } + + } + + } + + } + + watchersLock ( callback: Callback ): Promise { + + return this._watchersLock.then ( () => { + + return this._watchersLock = new Promise ( async resolve => { + + await callback (); + + resolve (); + + }); + + }); + + } + + watchersRestore (): void { + + delete this._watchersRestoreTimeout; + + const watchers = Object.entries ( this._watchersRestorable ); + + this._watchersRestorable = {}; + + for ( const [targetPath, config] of watchers ) { + + this.watchPath ( targetPath, config.options, config.handler ); + + } + + } + + async watcherAdd ( config: WatcherConfig, baseWatcherHandler?: WatcherHandler ): Promise { + + const {folderPath} = config; + + const configs = this._watchers[folderPath] = ( this._watchers[folderPath] || [] ); + + configs.push ( config ); + + const watcherHandler = new WatcherHandler ( this, config, baseWatcherHandler ); + + await watcherHandler.init (); + + return watcherHandler; + + } + + watcherClose ( config: WatcherConfig ): void { + + config.watcher.close (); + + const configs = this._watchers[config.folderPath]; + + if ( configs ) { + + const index = configs.indexOf ( config ); + + configs.splice ( index, 1 ); + + if ( !configs.length ) { + + delete this._watchers[config.folderPath]; + + } + + } + + const rootPath = config.filePath || config.folderPath; + const isRoot = this._roots.has ( rootPath ); + + if ( isRoot ) { + + this._watchersRestorable[rootPath] = config; + + if ( !this._watchersRestoreTimeout ) { + + this._watchersRestoreTimeout = Utils.lang.defer ( () => this.watchersRestore () ); + + } + + } + + } + + watcherExists ( folderPath: Path, options: WatcherOptions, handler: Handler, filePath?: Path ): boolean { + + const configsSibling = this._watchers[folderPath]; + + if ( !!configsSibling?.find ( config => config.handler === handler && ( !config.filePath || config.filePath === filePath ) && config.options.ignore === options.ignore && !!config.options.native === !!options.native && ( !options.recursive || config.options.recursive ) ) ) return true; + + let folderAncestorPath = path.dirname ( folderPath ); + + for ( let depth = 1; depth < Infinity; depth++ ) { + + const configsAncestor = this._watchers[folderAncestorPath]; + + if ( !!configsAncestor?.find ( config => ( depth === 1 || ( config.options.recursive && depth <= ( config.options.depth ?? DEPTH ) ) ) && config.handler === handler && ( !config.filePath || config.filePath === filePath ) && config.options.ignore === options.ignore && !!config.options.native === !!options.native && ( !options.recursive || ( config.options.recursive && ( HAS_NATIVE_RECURSION && config.options.native !== false ) ) ) ) ) return true; + + if ( !HAS_NATIVE_RECURSION ) break; // No other ancestor will possibly be found + + const folderAncestorPathNext = path.dirname ( folderPath ); + + if ( folderAncestorPath === folderAncestorPathNext ) break; + + folderAncestorPath = folderAncestorPathNext; + + } + + return false; + + } + + async watchDirectories ( foldersPaths: Path[], options: WatcherOptions, handler: Handler, filePath?: Path, baseWatcherHandler?: WatcherHandler ): Promise { + + if ( this.isClosed () ) return; + + foldersPaths = Utils.lang.uniq ( foldersPaths ).sort (); + + let watcherHandlerLast: WatcherHandler | undefined; + + for ( const folderPath of foldersPaths ) { + + if ( this.isIgnored ( folderPath, options.ignore ) ) continue; + + if ( this.watcherExists ( folderPath, options, handler, filePath ) ) continue; + + try { + + const watcherOptions = ( !options.recursive || ( HAS_NATIVE_RECURSION && options.native !== false ) ) ? options : { ...options, recursive: false }; // Ensuring recursion is explicitly disabled if not available + const watcher = fs.watch ( folderPath, watcherOptions ); + const watcherConfig: WatcherConfig = { watcher, handler, options, folderPath, filePath }; + const watcherHandler = watcherHandlerLast = await this.watcherAdd ( watcherConfig, baseWatcherHandler ); + + const isRoot = this._roots.has ( filePath || folderPath ); + + if ( isRoot ) { + + const parentOptions: WatcherOptions = { ...options, ignoreInitial: true, recursive: false }; // Ensuring only the parent folder is being watched + const parentFolderPath = path.dirname ( folderPath ); + const parentFilePath = folderPath; + + await this.watchDirectories ( [parentFolderPath], parentOptions, handler, parentFilePath, watcherHandler ); + + //TODO: Watch parents recursively with the following code, which requires other things to be changed too though + + // while ( true ) { + + // await this.watchDirectories ( [parentFolderPath], parentOptions, handler, parentFilePath, watcherHandler ); + + // const parentFolderPathNext = path.dirname ( parentFolderPath ); + + // if ( parentFolderPath === parentFolderPathNext ) break; + + // parentFilePath = parentFolderPath; + // parentFolderPath = parentFolderPathNext; + + // } + + } + + } catch ( error: unknown ) { + + this.error ( error ); + + } + + } + + return watcherHandlerLast; + + } + + async watchDirectory ( folderPath: Path, options: WatcherOptions, handler: Handler, filePath?: Path, baseWatcherHandler?: WatcherHandler ): Promise { + + if ( this.isClosed () ) return; + + if ( this.isIgnored ( folderPath, options.ignore ) ) return; + + if ( !options.recursive || ( HAS_NATIVE_RECURSION && options.native !== false ) ) { + + return this.watchersLock ( () => { + + return this.watchDirectories ( [folderPath], options, handler, filePath, baseWatcherHandler ); + + }); + + } else { + + options = { ...options, recursive: true }; // Ensuring recursion is explicitly enabled + + const depth = options.depth ?? DEPTH; + const limit = options.limit ?? LIMIT; + const [folderSubPaths] = await Utils.fs.readdir ( folderPath, options.ignore, depth, limit, this._closeSignal, options.readdirMap ); + + return this.watchersLock ( async () => { + + const watcherHandler = await this.watchDirectories ( [folderPath], options, handler, filePath, baseWatcherHandler ); + + if ( folderSubPaths.length ) { + + const folderPathDepth = Utils.fs.getDepth ( folderPath ); + + for ( const folderSubPath of folderSubPaths ) { + + const folderSubPathDepth = Utils.fs.getDepth ( folderSubPath ); + const subDepth = Math.max ( 0, depth - ( folderSubPathDepth - folderPathDepth ) ); + const subOptions = { ...options, depth: subDepth }; // Updating the maximum depth to account for depth of the sub path + + await this.watchDirectories ( [folderSubPath], subOptions, handler, filePath, baseWatcherHandler || watcherHandler ); + + } + + } + + }); + + } + + } + + async watchFileOnce ( filePath: Path, options: WatcherOptions, callback: Callback ): Promise { + + if ( this.isClosed () ) return; + + options = { ...options, ignoreInitial: false }; // Ensuring initial events are detected too + + if ( this.subwatcherExists ( filePath, options ) ) return; + + const config: SubwatcherConfig = { targetPath: filePath, options }; + + const handler = ( event: TargetEvent, targetPath: Path ) => { + if ( targetPath !== filePath ) return; + stop (); + callback (); + }; + + const watcher = new Watcher ( handler ); + + const start = (): void => { + this._subwatchers.add ( config ); + this.on ( WatcherEvent.CLOSE, stop ); // Ensuring the subwatcher is stopped on close + watcher.watchFile ( filePath, options, handler ); + }; + + const stop = (): void => { + this._subwatchers.delete ( config ); + this.removeListener ( WatcherEvent.CLOSE, stop ); // Ensuring there are no leftover listeners + watcher.close (); + }; + + return start (); + + } + + async watchFile ( filePath: Path, options: WatcherOptions, handler: Handler ): Promise { + + if ( this.isClosed () ) return; + + if ( this.isIgnored ( filePath, options.ignore ) ) return; + + options = { ...options, recursive: false }; // Ensuring recursion is explicitly disabled + + const folderPath = path.dirname ( filePath ); + + return this.watchDirectory ( folderPath, options, handler, filePath ); + + } + + async watchPollingOnce ( targetPath: Path, options: WatcherOptions, callback: Callback ): Promise { + + if ( this.isClosed () ) return; + + let isDone = false; + + const poller = new WatcherPoller (); + + const disposer = await this.watchPolling ( targetPath, options, async () => { + + if ( isDone ) return; + + const events = await poller.update ( targetPath, options.pollingTimeout ); + + if ( !events.length ) return; // Nothing actually changed, skipping + + if ( isDone ) return; // Another async callback has done the work already, skipping + + isDone = true; + + disposer (); + + callback (); + + }); + + } + + async watchPolling ( targetPath: Path, options: WatcherOptions, callback: Callback ): Promise { + + if ( this.isClosed () ) return Utils.lang.noop; + + if ( this.pollerExists ( targetPath, options ) ) return Utils.lang.noop; + + const watcherOptions = { ...options, interval: options.pollingInterval ?? POLLING_INTERVAL }; // Ensuring a default interval is set + + const config: PollerConfig = { targetPath, options }; + + const start = (): void => { + this._pollers.add ( config ); + this.on ( WatcherEvent.CLOSE, stop ); // Ensuring polling is stopped on close + fs.watchFile ( targetPath, watcherOptions, callback ); + }; + + const stop = (): void => { + this._pollers.delete ( config ); + this.removeListener ( WatcherEvent.CLOSE, stop ); // Ensuring there are no leftover listeners + fs.unwatchFile ( targetPath, callback ); + }; + + Utils.lang.attempt ( start ); + + return () => Utils.lang.attempt ( stop ); + + } + + async watchUnknownChild ( targetPath: Path, options: WatcherOptions, handler: Handler ): Promise { + + if ( this.isClosed () ) return; + + const watch = () => this.watchPath ( targetPath, options, handler ); + + return this.watchFileOnce ( targetPath, options, watch ); + + } + + async watchUnknownTarget ( targetPath: Path, options: WatcherOptions, handler: Handler ): Promise { + + if ( this.isClosed () ) return; + + const watch = () => this.watchPath ( targetPath, options, handler ); + + return this.watchPollingOnce ( targetPath, options, watch ); + + } + + async watchPaths ( targetPaths: Path[], options: WatcherOptions, handler: Handler ): Promise { + + if ( this.isClosed () ) return; + + targetPaths = Utils.lang.uniq ( targetPaths ).sort (); + + const isParallelizable = targetPaths.every ( ( targetPath, index ) => targetPaths.every ( ( t, i ) => i === index || !Utils.fs.isSubPath ( targetPath, t ) ) ); // All paths are about separate subtrees, so we can start watching in parallel safely //TODO: Find parallelizable chunks rather than using an all or nothing approach + + if ( isParallelizable ) { // Watching in parallel + + await Promise.all ( targetPaths.map ( targetPath => { + + return this.watchPath ( targetPath, options, handler ); + + })); + + } else { // Watching serially + + for ( const targetPath of targetPaths ) { + + await this.watchPath ( targetPath, options, handler ); + + } + + } + + } + + async watchPath ( targetPath: Path, options: WatcherOptions, handler: Handler ): Promise { + + if ( this.isClosed () ) return; + + targetPath = path.resolve ( targetPath ); + + if ( this.isIgnored ( targetPath, options.ignore ) ) return; + + const stats = await Utils.fs.poll ( targetPath, options.pollingTimeout ); + + if ( !stats ) { + + const parentPath = path.dirname ( targetPath ); + const parentStats = await Utils.fs.poll ( parentPath, options.pollingTimeout ); + + if ( parentStats?.isDirectory () ) { + + return this.watchUnknownChild ( targetPath, options, handler ); + + } else { + + return this.watchUnknownTarget ( targetPath, options, handler ); + + } + + } else if ( stats.isFile () ) { + + return this.watchFile ( targetPath, options, handler ); + + } else if ( stats.isDirectory () ) { + + return this.watchDirectory ( targetPath, options, handler ); + + } else { + + this.error ( `"${targetPath}" is not supported` ); + + } + + } + + async watch ( target?: Path[] | Path | Handler, options?: WatcherOptions | Handler, handler: Handler = Utils.lang.noop ): Promise { + + if ( Utils.lang.isFunction ( target ) ) return this.watch ( [], {}, target ); + + if ( Utils.lang.isUndefined ( target ) ) return this.watch ( [], options, handler ); + + if ( Utils.lang.isFunction ( options ) ) return this.watch ( target, {}, options ); + + if ( Utils.lang.isUndefined ( options ) ) return this.watch ( target, {}, handler ); + + if ( this.isClosed () ) return; + + if ( this.isReady () ) options.readdirMap = undefined; // Only usable before initialization + + const targetPaths = Utils.lang.castArray ( target ); + + targetPaths.forEach ( targetPath => this._roots.add ( targetPath ) ); + + await this.watchPaths ( targetPaths, options, handler ); + + if ( this.isClosed () ) return; + + if ( handler !== Utils.lang.noop ) { + + this.on ( WatcherEvent.ALL, handler ); + + } + + options.readdirMap = undefined; // Only usable before initialization + + this.ready (); + + } + +} + +/* EXPORT */ + +export default Watcher; diff --git a/ts/watcher_handler.ts b/ts/watcher_handler.ts new file mode 100644 index 0000000..bcd8455 --- /dev/null +++ b/ts/watcher_handler.ts @@ -0,0 +1,427 @@ + +/* 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'; +import type Watcher from './watcher.js'; +import type {Event, FSWatcher, Handler, HandlerBatched, Path, WatcherOptions, WatcherConfig} from './types.js'; + +/* MAIN */ + +class WatcherHandler { + + /* VARIABLES */ + + base?: WatcherHandler; + watcher: Watcher; + handler: Handler; + handlerBatched: HandlerBatched; + fswatcher: FSWatcher; + options: WatcherOptions; + folderPath: Path; + filePath?: Path; + + /* CONSTRUCTOR */ + + constructor ( watcher: Watcher, config: WatcherConfig, base?: WatcherHandler ) { + + 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: Path ): boolean { // Only events inside the watched root are emitted + + if ( this.filePath ) { + + return targetPath === this.filePath; + + } else { + + return targetPath === this.folderPath || Utils.fs.isSubPath ( this.folderPath, targetPath ); + + } + + } + + _makeHandlerBatched ( delay: number = DEBOUNCE ) { + + return (() => { + + let lock = this.watcher._readyWait; // ~Ensuring no two flushes are active in parallel, or before the watcher is ready + let initials: Event[] = []; + let regulars: Set = new Set (); + + const flush = async ( initials: Event[], regulars: Set ): Promise => { + + 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: FSTargetEvent, targetPath: Path = '', isInitial: boolean = false ): Promise => { + + if ( isInitial ) { // Poll immediately + + await this.eventsPopulate ( [targetPath], initials, true ); + + } else { // Poll later + + regulars.add ( targetPath ); + + } + + lock.then ( flushDebounced ); + + }; + + })(); + + } + + /* EVENT HELPERS */ + + eventsDeduplicate ( events: Event[] ): Event[] { + + if ( events.length < 2 ) return events; + + const targetsEventPrev: Record = {}; + + 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: Path[], events: Event[] = [], isInitial: boolean = false ): Promise { + + 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: Path[], targetPath: Path, events: Event[] = [], isInitial: boolean = false ): Promise { + + 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: Path[], targetPath: Path, events: Event[] = [], isInitial: boolean = false ): Promise { + + 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: Path ): void { + + 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: Path ): void { + + 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: Path ): void { + + if ( this._isSubRoot ( targetPath ) ) { + + this.watcher.event ( TargetEvent.CHANGE, targetPath ); + + } + + } + + onTargetUnlink ( targetPath: Path ): void { + + 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: Path ): void { + + 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: Event ): void { + + 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: Event[] ): void { + + for ( const event of events ) { + + this.onTargetEvent ( event ); + + } + + } + + onWatcherEvent ( event?: FSTargetEvent, targetPath?: Path, isInitial: boolean = false ): Promise { + + return this.handlerBatched ( event, targetPath, isInitial ); + + } + + onWatcherChange ( event: FSTargetEvent = FSTargetEvent.CHANGE, targetName?: string | null ): void { + + 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: NodeJS.ErrnoException ): void { + + 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 (): Promise { + + await this.initWatcherEvents (); + await this.initInitialEvents (); + + } + + async initWatcherEvents (): Promise { + + 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 (): Promise { + + 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; diff --git a/ts/watcher_locker.ts b/ts/watcher_locker.ts new file mode 100644 index 0000000..f69fe33 --- /dev/null +++ b/ts/watcher_locker.ts @@ -0,0 +1,202 @@ + +/* IMPORT */ + +import {RENAME_TIMEOUT} from './constants.js'; +import {FileType, TargetEvent} from './enums.js'; +import Utils from './utils.js'; +import WatcherLocksResolver from './watcher_locks_resolver.js'; +import type Watcher from './watcher.js'; +import type {Path, LocksAdd, LocksUnlink, LocksPair, LockConfig} from './types.js'; + +/* MAIN */ + +//TODO: Use a better name for this thing, maybe "RenameDetector" + +class WatcherLocker { + + /* VARIABLES */ + + _locksAdd!: LocksAdd; + _locksAddDir!: LocksAdd; + _locksUnlink!: LocksUnlink; + _locksUnlinkDir!: LocksUnlink; + _locksDir!: LocksPair; + _locksFile!: LocksPair; + _watcher: Watcher; + + static DIR_EVENTS = { + add: TargetEvent.ADD_DIR, + rename: TargetEvent.RENAME_DIR, + unlink: TargetEvent.UNLINK_DIR + }; + + static FILE_EVENTS = { + add: TargetEvent.ADD, + change: TargetEvent.CHANGE, + rename: TargetEvent.RENAME, + unlink: TargetEvent.UNLINK + }; + + /* CONSTRUCTOR */ + + constructor ( watcher: Watcher ) { + + this._watcher = watcher; + + this.reset (); + + } + + /* API */ + + getLockAdd ( config: LockConfig, timeout: number = RENAME_TIMEOUT ): void { + + const {ino, targetPath, events, locks} = config; + + const emit = (): void => { + const otherPath = this._watcher._poller.paths.find ( ino || -1, path => path !== targetPath ); // Maybe this is actually a rename in a case-insensitive filesystem + if ( otherPath && otherPath !== targetPath ) { + if ( Utils.fs.getRealPath ( targetPath, true ) === otherPath ) return; //TODO: This seems a little too special-casey + this._watcher.event ( events.rename, otherPath, targetPath ); + } else { + this._watcher.event ( events.add, targetPath ); + } + }; + + if ( !ino ) return emit (); + + const cleanup = (): void => { + locks.add.delete ( ino ); + WatcherLocksResolver.remove ( free ); + }; + + const free = (): void => { + cleanup (); + emit (); + }; + + WatcherLocksResolver.add ( free, timeout ); + + const resolve = (): void => { + const unlink = locks.unlink.get ( ino ); + if ( !unlink ) return; // No matching "unlink" lock found, skipping + cleanup (); + const targetPathPrev = unlink (); + if ( targetPath === targetPathPrev ) { + if ( events.change ) { + if ( this._watcher._poller.stats.has ( targetPath ) ) { + this._watcher.event ( events.change, targetPath ); + } + } + } else { + this._watcher.event ( events.rename, targetPathPrev, targetPath ); + } + }; + + locks.add.set ( ino, resolve ); + + resolve (); + + } + + getLockUnlink ( config: LockConfig, timeout: number = RENAME_TIMEOUT ): void { + + const {ino, targetPath, events, locks} = config; + + const emit = (): void => { + this._watcher.event ( events.unlink, targetPath ); + }; + + if ( !ino ) return emit (); + + const cleanup = (): void => { + locks.unlink.delete ( ino ); + WatcherLocksResolver.remove ( free ); + }; + + const free = (): void => { + cleanup (); + emit (); + }; + + WatcherLocksResolver.add ( free, timeout ); + + const overridden = (): Path => { + cleanup (); + return targetPath; + }; + + locks.unlink.set ( ino, overridden ); + + locks.add.get ( ino )?.(); + + } + + getLockTargetAdd ( targetPath: Path, timeout?: number ): void { + + const ino = this._watcher._poller.getIno ( targetPath, TargetEvent.ADD, FileType.FILE ); + + return this.getLockAdd ({ + ino, + targetPath, + events: WatcherLocker.FILE_EVENTS, + locks: this._locksFile + }, timeout ); + + } + + getLockTargetAddDir ( targetPath: Path, timeout?: number ): void { + + const ino = this._watcher._poller.getIno ( targetPath, TargetEvent.ADD_DIR, FileType.DIR ); + + return this.getLockAdd ({ + ino, + targetPath, + events: WatcherLocker.DIR_EVENTS, + locks: this._locksDir + }, timeout ); + + } + + getLockTargetUnlink ( targetPath: Path, timeout?: number ): void { + + const ino = this._watcher._poller.getIno ( targetPath, TargetEvent.UNLINK, FileType.FILE ); + + return this.getLockUnlink ({ + ino, + targetPath, + events: WatcherLocker.FILE_EVENTS, + locks: this._locksFile + }, timeout ); + + } + + getLockTargetUnlinkDir ( targetPath: Path, timeout?: number ): void { + + const ino = this._watcher._poller.getIno ( targetPath, TargetEvent.UNLINK_DIR, FileType.DIR ); + + return this.getLockUnlink ({ + ino, + targetPath, + events: WatcherLocker.DIR_EVENTS, + locks: this._locksDir + }, timeout ); + + } + + reset (): void { + + this._locksAdd = new Map (); + this._locksAddDir = new Map (); + this._locksUnlink = new Map (); + this._locksUnlinkDir = new Map (); + this._locksDir = { add: this._locksAddDir, unlink: this._locksUnlinkDir }; + this._locksFile = { add: this._locksAdd, unlink: this._locksUnlink }; + + } + +} + +/* EXPORT */ + +export default WatcherLocker; diff --git a/ts/watcher_locks_resolver.ts b/ts/watcher_locks_resolver.ts new file mode 100644 index 0000000..f99838d --- /dev/null +++ b/ts/watcher_locks_resolver.ts @@ -0,0 +1,73 @@ + +/* MAIN */ + +// Registering a single interval scales much better than registering N timeouts +// Timeouts are respected within the interval margin + +const WatcherLocksResolver = { + + /* VARIABLES */ + + interval: 100, + intervalId: undefined as NodeJS.Timeout | undefined, + fns: new Map (), + + /* LIFECYCLE API */ + + init: (): void => { + + if ( WatcherLocksResolver.intervalId ) return; + + WatcherLocksResolver.intervalId = setInterval ( WatcherLocksResolver.resolve, WatcherLocksResolver.interval ); + + }, + + reset: (): void => { + + if ( !WatcherLocksResolver.intervalId ) return; + + clearInterval ( WatcherLocksResolver.intervalId ); + + delete WatcherLocksResolver.intervalId; + + }, + + /* API */ + + add: ( fn: Function, timeout: number ): void => { + + WatcherLocksResolver.fns.set ( fn, Date.now () + timeout ); + + WatcherLocksResolver.init (); + + }, + + remove: ( fn: Function ): void => { + + WatcherLocksResolver.fns.delete ( fn ); + + }, + + resolve: (): void => { + + if ( !WatcherLocksResolver.fns.size ) return WatcherLocksResolver.reset (); + + const now = Date.now (); + + for ( const [fn, timestamp] of WatcherLocksResolver.fns ) { + + if ( timestamp >= now ) continue; // We should still wait some more for this + + WatcherLocksResolver.remove ( fn ); + + fn (); + + } + + } + +}; + +/* EXPORT */ + +export default WatcherLocksResolver; diff --git a/ts/watcher_poller.ts b/ts/watcher_poller.ts new file mode 100644 index 0000000..9134f0e --- /dev/null +++ b/ts/watcher_poller.ts @@ -0,0 +1,193 @@ + +/* IMPORT */ + +import {FileType, TargetEvent} from './enums.js'; +import LazyMapSet from './lazy_map_set.js'; +import Utils from './utils.js'; +import WatcherStats from './watcher_stats.js'; +import type {INO, Path} from './types.js'; + +/* MAIN */ + +class WatcherPoller { + + /* VARIABLES */ + + inos: Partial>> = {}; + paths: LazyMapSet = new LazyMapSet (); + stats: Map = new Map (); + + /* API */ + + getIno ( targetPath: Path, event: TargetEvent, type?: FileType ): INO | undefined { + + const inos = this.inos[event]; + + if ( !inos ) return; + + const ino = inos[targetPath]; + + if ( !ino ) return; + + if ( type && ino[1] !== type ) return; + + return ino[0]; + + } + + getStats ( targetPath: Path ): WatcherStats | undefined { + + return this.stats.get ( targetPath ); + + } + + async poll ( targetPath: Path, timeout?: number ): Promise { + + const stats = await Utils.fs.poll ( targetPath, timeout ); + + if ( !stats ) return; + + const isSupported = stats.isFile () || stats.isDirectory (); + + if ( !isSupported ) return; + + return new WatcherStats ( stats ); + + } + + reset (): void { + + this.inos = {}; + this.paths = new LazyMapSet (); + this.stats = new Map (); + + } + + async update ( targetPath: Path, timeout?: number ): Promise { + + const prev = this.getStats ( targetPath ); + const next = await this.poll ( targetPath, timeout ); + + this.updateStats ( targetPath, next ); + + if ( !prev && next ) { + + if ( next.isFile () ) { + + this.updateIno ( targetPath, TargetEvent.ADD, next ); + + return [TargetEvent.ADD]; + + } + + if ( next.isDirectory () ) { + + this.updateIno ( targetPath, TargetEvent.ADD_DIR, next ); + + return [TargetEvent.ADD_DIR]; + + } + + } else if ( prev && !next ) { + + if ( prev.isFile () ) { + + this.updateIno ( targetPath, TargetEvent.UNLINK, prev ); + + return [TargetEvent.UNLINK]; + + } + + if ( prev.isDirectory () ) { + + this.updateIno ( targetPath, TargetEvent.UNLINK_DIR, prev ); + + return [TargetEvent.UNLINK_DIR]; + + } + + } else if ( prev && next ) { + + if ( prev.isFile () ) { + + if ( next.isFile () ) { + + if ( prev.ino === next.ino && !prev.size && !next.size ) return []; // Same path, same content and same file, nothing actually changed + + this.updateIno ( targetPath, TargetEvent.CHANGE, next ); + + return [TargetEvent.CHANGE]; + + } + + if ( next.isDirectory () ) { + + this.updateIno ( targetPath, TargetEvent.UNLINK, prev ); + this.updateIno ( targetPath, TargetEvent.ADD_DIR, next ); + + return [TargetEvent.UNLINK, TargetEvent.ADD_DIR]; + + } + + } else if ( prev.isDirectory () ) { + + if ( next.isFile () ) { + + this.updateIno ( targetPath, TargetEvent.UNLINK_DIR, prev ); + this.updateIno ( targetPath, TargetEvent.ADD, next ); + + return [TargetEvent.UNLINK_DIR, TargetEvent.ADD]; + + } + + if ( next.isDirectory () ) { + + if ( prev.ino === next.ino ) return []; // Same path and same directory, nothing actually changed + + this.updateIno ( targetPath, TargetEvent.UNLINK_DIR, prev ); + this.updateIno ( targetPath, TargetEvent.ADD_DIR, next ); + + return [TargetEvent.UNLINK_DIR, TargetEvent.ADD_DIR]; + + } + + } + + } + + return []; + + } + + updateIno ( targetPath: Path, event: TargetEvent, stats: WatcherStats ): void { + + const inos = this.inos[event] = this.inos[event] || ( this.inos[event] = {} ); + const type = stats.isFile () ? FileType.FILE : FileType.DIR; + + inos[targetPath] = [stats.ino, type]; + + } + + updateStats ( targetPath: Path, stats?: WatcherStats ): void { + + if ( stats ) { + + this.paths.set ( stats.ino, targetPath ); + this.stats.set ( targetPath, stats ); + + } else { + + const ino = this.stats.get ( targetPath )?.ino || -1; + + this.paths.delete ( ino, targetPath ); + this.stats.delete ( targetPath ); + + } + + } + +} + +/* EXPORT */ + +export default WatcherPoller; diff --git a/ts/watcher_stats.ts b/ts/watcher_stats.ts new file mode 100644 index 0000000..0a2ff82 --- /dev/null +++ b/ts/watcher_stats.ts @@ -0,0 +1,64 @@ + +/* IMPORT */ + +import type {INO, Stats} from './types.js'; + +/* MAIN */ + +// An more memory-efficient representation of the useful subset of stats objects + +class WatcherStats { + + /* VARIABLES */ + + ino: INO; + size: number; + atimeMs: number; + mtimeMs: number; + ctimeMs: number; + birthtimeMs: number; + _isFile: boolean; + _isDirectory: boolean; + _isSymbolicLink: boolean; + + /* CONSTRUCTOR */ + + constructor ( stats: Stats ) { + + this.ino = ( stats.ino <= Number.MAX_SAFE_INTEGER ) ? Number ( stats.ino ) : stats.ino; + this.size = Number ( stats.size ); + this.atimeMs = Number ( stats.atimeMs ); + this.mtimeMs = Number ( stats.mtimeMs ); + this.ctimeMs = Number ( stats.ctimeMs ); + this.birthtimeMs = Number ( stats.birthtimeMs ); + this._isFile = stats.isFile (); + this._isDirectory = stats.isDirectory (); + this._isSymbolicLink = stats.isSymbolicLink (); + + } + + /* API */ + + isFile (): boolean { + + return this._isFile; + + } + + isDirectory (): boolean { + + return this._isDirectory; + + } + + isSymbolicLink (): boolean { + + return this._isSymbolicLink; + + } + +} + +/* EXPORT */ + +export default WatcherStats; diff --git a/tsconfig.json b/tsconfig.json new file mode 100755 index 0000000..dfe5a55 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "experimentalDecorators": true, + "useDefineForClassFields": false, + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "verbatimModuleSyntax": true + }, + "exclude": [ + "dist_*/**/*.d.ts" + ] +}