diff --git a/packages/cached/test/cached-fs.spec.ts b/packages/cached/test/cached-fs.spec.ts index 2a84fc3c..f5e89ae5 100644 --- a/packages/cached/test/cached-fs.spec.ts +++ b/packages/cached/test/cached-fs.spec.ts @@ -232,7 +232,6 @@ describe("createCachedFs", () => { const testProvider = async () => { const fs = createCachedFs(createMemoryFs()); - fs.watchService.addGlobalListener(({ path }) => fs.invalidate(path)); return { fs, dispose: async () => undefined, diff --git a/packages/memory/src/memory-fs.ts b/packages/memory/src/memory-fs.ts index e0e86a12..a8e1d0ee 100644 --- a/packages/memory/src/memory-fs.ts +++ b/packages/memory/src/memory-fs.ts @@ -5,11 +5,9 @@ import { type IDirectoryContents, type IDirectoryEntry, type IFileSystemStats, - type IWatchEvent, type ReadFileOptions, type RmOptions, type StatSyncOptions, - type WatchEventListener, type WatchChangeEventListener, } from "@file-services/types"; import { SetMultiMap, createFileSystem, syncToAsyncFs } from "@file-services/utils"; @@ -65,8 +63,6 @@ export function createBaseMemoryFsSync(): IBaseMemFileSystemSync { const changeListeners = new SetMultiMap(); const recursiveChangeListeners = new SetMultiMap(); const closeListeners = new SetMultiMap void>(); - const pathListeners = new SetMultiMap(); - const globalListeners = new Set(); const textEncoder = new TextEncoder(); realpathSync.native = realpathSync; @@ -75,34 +71,6 @@ export function createBaseMemoryFsSync(): IBaseMemFileSystemSync { root, ...posixPath, resolve: resolvePath, - watchService: { - async watchPath(path, listener) { - const resolvedPath = resolvePath(path); - if (listener) { - pathListeners.add(resolvedPath, listener); - } - }, - async unwatchPath(path, listener) { - const resolvedPath = resolvePath(path); - if (listener) { - pathListeners.delete(resolvedPath, listener); - } else { - pathListeners.deleteKey(resolvedPath); - } - }, - async unwatchAllPaths() { - pathListeners.clear(); - }, - addGlobalListener(listener) { - globalListeners.add(listener); - }, - removeGlobalListener(listener) { - globalListeners.delete(listener); - }, - clearGlobalListeners() { - globalListeners.clear(); - }, - }, caseSensitive: true, cwd, chdir, @@ -212,7 +180,6 @@ export function createBaseMemoryFsSync(): IBaseMemFileSystemSync { } existingNode.entry = { ...existingNode.entry, mtime: new Date() }; existingNode.contents = typeof fileContent === "string" ? fileContent : new Uint8Array(fileContent); - emitWatchEvent({ path: resolvedPath, stats: existingNode.entry }); emitChangeEvent("change", resolvedPath); } else { const parentPath = posixPath.dirname(resolvedPath); @@ -238,7 +205,6 @@ export function createBaseMemoryFsSync(): IBaseMemFileSystemSync { contents: typeof fileContent === "string" ? fileContent : new Uint8Array(fileContent), }; parentNode.contents.set(fileName, newFileNode); - emitWatchEvent({ path: resolvedPath, stats: newFileNode.entry }); emitChangeEvent("rename", resolvedPath); } } @@ -262,7 +228,6 @@ export function createBaseMemoryFsSync(): IBaseMemFileSystemSync { } parentNode.contents.delete(fileName); - emitWatchEvent({ path: resolvedPath, stats: null }); emitChangeEvent("rename", resolvedPath); } @@ -327,7 +292,6 @@ export function createBaseMemoryFsSync(): IBaseMemFileSystemSync { const newDirNode: IFsMemDirectoryNode = createMemDirectory(directoryName); parentNode.contents.set(directoryName, newDirNode); - emitWatchEvent({ path: resolvedPath, stats: newDirNode.entry }); emitChangeEvent("rename", resolvedPath); } @@ -350,7 +314,6 @@ export function createBaseMemoryFsSync(): IBaseMemFileSystemSync { } parentNode.contents.delete(directoryName); - emitWatchEvent({ path: resolvedPath, stats: null }); emitChangeEvent("rename", resolvedPath); } @@ -382,7 +345,6 @@ export function createBaseMemoryFsSync(): IBaseMemFileSystemSync { } parentNode.contents.delete(targetName); - emitWatchEvent({ path: resolvedPath, stats: null }); emitChangeEvent("rename", resolvedPath); } @@ -508,8 +470,6 @@ export function createBaseMemoryFsSync(): IBaseMemFileSystemSync { sourceNode.entry = { ...sourceNode.entry, name: destinationName, mtime: new Date() }; destinationParentNode.contents.set(destinationName, sourceNode); - emitWatchEvent({ path: resolvedSourcePath, stats: null }); - emitWatchEvent({ path: resolvedDestinationPath, stats: sourceNode.entry }); emitChangeEvent("rename", resolvedSourcePath); emitChangeEvent("rename", resolvedDestinationPath); } @@ -555,7 +515,6 @@ export function createBaseMemoryFsSync(): IBaseMemFileSystemSync { }; destParentNode.contents.set(targetName, newFileNode); - emitWatchEvent({ path: resolvedDestinationPath, stats: newFileNode.entry }); emitChangeEvent("rename", resolvedDestinationPath); } @@ -591,7 +550,6 @@ export function createBaseMemoryFsSync(): IBaseMemFileSystemSync { }; parentNode.contents.set(fileName, symlinkNode); - emitWatchEvent({ path: resolvedLinkPath, stats: symlinkNode.entry }); emitChangeEvent("rename", resolvedLinkPath); } @@ -655,18 +613,6 @@ export function createBaseMemoryFsSync(): IBaseMemFileSystemSync { } } } - - function emitWatchEvent(watchEvent: IWatchEvent): void { - for (const listener of globalListeners) { - listener(watchEvent); - } - const listeners = pathListeners.get(watchEvent.path); - if (listeners) { - for (const listener of listeners) { - listener(watchEvent); - } - } - } } function createMemDirectory(name: string): IFsMemDirectoryNode { diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 9625ec34..924d023d 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -2,7 +2,6 @@ import type { IFileSystem } from "@file-services/types"; import { createNodeFs } from "./node-fs"; export * from "./node-fs"; -export * from "./watch-service"; export const nodeFs: IFileSystem = createNodeFs(); export default nodeFs; diff --git a/packages/node/src/node-fs.ts b/packages/node/src/node-fs.ts index 5e8e8f54..f0a34b4e 100644 --- a/packages/node/src/node-fs.ts +++ b/packages/node/src/node-fs.ts @@ -5,28 +5,22 @@ import { promisify } from "node:util"; import type { IBaseFileSystem, IFileSystem, WatchOptions } from "@file-services/types"; import { createFileSystem } from "@file-services/utils"; -import { INodeWatchServiceOptions, NodeWatchService } from "./watch-service"; import { RecursiveFSWatcher } from "./recursive-fs-watcher"; const caseSensitive = !fs.existsSync(argv[0]!.toUpperCase()); const fsPromisesExists = promisify(fs.exists); -export interface ICreateNodeFsOptions { - watchOptions?: INodeWatchServiceOptions; +export function createNodeFs(): IFileSystem { + return createFileSystem(createBaseNodeFs()); } -export function createNodeFs(options?: ICreateNodeFsOptions): IFileSystem { - return createFileSystem(createBaseNodeFs(options)); -} - -export function createBaseNodeFs(options?: ICreateNodeFsOptions): IBaseFileSystem { +export function createBaseNodeFs(): IBaseFileSystem { const originalWatch = fs.watch; const watch = process.platform === "linux" ? wrapWithOwnRecursiveImpl(originalWatch) : originalWatch; return { ...path, chdir, cwd, - watchService: new NodeWatchService(options && options.watchOptions), caseSensitive, ...fs, watch, diff --git a/packages/node/src/watch-service.ts b/packages/node/src/watch-service.ts deleted file mode 100644 index d2b84a11..00000000 --- a/packages/node/src/watch-service.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { once } from "node:events"; -import { FSWatcher, promises as fsPromises, watch } from "node:fs"; -import { join } from "node:path"; - -import type { IFileSystemStats, IWatchEvent, IWatchService, WatchEventListener } from "@file-services/types"; -import { SetMultiMap } from "@file-services/utils"; - -const { stat } = fsPromises; - -export interface INodeWatchServiceOptions { - /** - * Should fs watchers be persistent and keep the process open - * (until someone calls `unwatchAllPaths()`) - * - * @default true - */ - persistent?: boolean; - - /** - * How much time (in ms) to wait for next native watch event before - * emitting a service watch event - * - * @default 200 - */ - debounceWait?: number; -} - -interface IPendingEvent { - /* whether one of the raw fs events was 'rename' */ - renamed: boolean; - - /* id of the setTimeout call, for debouncing */ - timerId: ReturnType; -} - -export class NodeWatchService implements IWatchService { - /** user's subsribed global listeners */ - private globalListeners: Set = new Set(); - - /** resolved options (default + user) */ - private options: Required; - - /** all watched paths (including files inside watched directories) */ - private watchedPaths = new SetMultiMap(); - - /** path to actual FSWatcher instance opened for it */ - private fsWatchers = new Map(); - - /** path to its pending event (debounced watch event) */ - private pendingEvents = new Map(); - - /** - * Construct a new Node file system watch service - */ - constructor(options?: INodeWatchServiceOptions) { - this.options = { persistent: true, debounceWait: 200, ...options }; - } - - public async watchPath(path: string, listener?: WatchEventListener): Promise { - if (listener) { - this.watchedPaths.add(path, listener); - } - await this.ensureFsWatcher(path); - } - - public async unwatchPath(path: string, listener?: WatchEventListener): Promise { - if (listener) { - this.watchedPaths.delete(path, listener); - } else { - this.watchedPaths.deleteKey(path); - } - - if (!this.watchedPaths.hasKey(path)) { - const fsWatcher = this.fsWatchers.get(path); - if (fsWatcher) { - fsWatcher.close(); - this.fsWatchers.delete(path); - await once(fsWatcher, "close"); - } - } - const pendingEvent = this.pendingEvents.get(path); - if (pendingEvent) { - clearTimeout(pendingEvent.timerId); - this.pendingEvents.delete(path); - } - } - - public async unwatchAllPaths(): Promise { - for (const watcher of this.fsWatchers.values()) { - watcher.close(); - } - for (const { timerId } of this.pendingEvents.values()) { - clearTimeout(timerId); - } - const watcherCloseEvents = Array.from(this.fsWatchers.values(), (watcher) => once(watcher, "close")); - this.pendingEvents.clear(); - this.fsWatchers.clear(); - this.watchedPaths.clear(); - await Promise.all(watcherCloseEvents); - } - - public addGlobalListener(listener: WatchEventListener): void { - this.globalListeners.add(listener); - } - - public removeGlobalListener(listener: WatchEventListener): void { - this.globalListeners.delete(listener); - } - - public clearGlobalListeners(): void { - this.globalListeners.clear(); - } - - /** - * Debounces watch events while retaining whether one of - * them was a 'rename' event - */ - private onPathEvent(eventType: string, eventPath: string) { - const pendingEvent = this.pendingEvents.get(eventPath); - const timerId = setTimeout(() => { - this.emitEvent(eventPath).catch((e) => this.onWatchError(e as Error, eventPath)); - }, this.options.debounceWait); - - if (pendingEvent) { - clearTimeout(pendingEvent.timerId); - pendingEvent.renamed = pendingEvent.renamed || eventType === "rename"; - pendingEvent.timerId = timerId; - } else { - this.pendingEvents.set(eventPath, { renamed: eventType === "rename", timerId }); - } - } - - private async emitEvent(path: string): Promise { - const pendingEvent = this.pendingEvents.get(path); - if (!pendingEvent) { - return; - } - this.pendingEvents.delete(path); - - const stats = await this.statSafe(path); - - if (pendingEvent.renamed) { - // if one of the bounced events was a rename, make sure to unwatch, - // as the underlying native inode is now different, and our watcher - // is not receiving events for the new one - const existingWatcher = this.fsWatchers.get(path); - if (existingWatcher) { - existingWatcher.close(); - this.fsWatchers.delete(path); - // rewatch if path points to a new inode - if (stats) { - await this.ensureFsWatcher(path, stats); - } - } - } - - const watchEvent: IWatchEvent = { path, stats }; - - // inform global listeners - for (const listener of this.globalListeners) { - listener(watchEvent); - } - - // inform path listeners - const listeners = this.watchedPaths.get(path); - if (listeners) { - for (const listener of listeners) { - listener({ path, stats }); - } - } - } - - private async ensureFsWatcher(path: string, stats?: IFileSystemStats) { - if (this.fsWatchers.has(path)) { - return; - } - - // accepting the optional stats saves us getting the stats ourselves - const pathStats = stats || (await this.statSafe(path)); - if (!pathStats) { - throw new Error(`cannot watch non-existing path: ${path}`); - } - - // open fsWatcher - const watchOptions = { persistent: this.options.persistent }; - if (pathStats.isFile()) { - const fileWatcher = watch(path, watchOptions, (type) => this.onPathEvent(type, path)); - fileWatcher.once("error", (e) => { - this.onWatchError(e, path); - }); - this.fsWatchers.set(path, fileWatcher); - } else if (pathStats.isDirectory()) { - const directoryWatcher = watch(path, watchOptions, (type, fileName) => { - if (fileName !== null) { - this.onDirectoryEvent(type, path, fileName).catch((e) => { - this.onWatchError(e, path); - }); - } - }); - directoryWatcher.once("error", (e) => { - this.onWatchError(e, path); - }); - this.fsWatchers.set(path, directoryWatcher); - } else { - throw new Error(`${path} does not point to a file or a directory`); - } - } - - private onWatchError(_e: unknown, path: string) { - this.onPathEvent("rename", path); - const watcher = this.fsWatchers.get(path); - if (watcher) { - watcher.close(); - this.fsWatchers.delete(path); - } - } - - private async onDirectoryEvent(eventType: string, directoryPath: string, fileName: string) { - // we must stats the directory, as the raw event gives us no indication - // whether an inner file or the directory itself was removed. - // Upon removal of the directory itself, the fileName parameter is just the directory name, - // which can also be interpreted as an inner file with that name being removed. - const directoryStats = await this.statSafe(directoryPath); - this.onPathEvent(eventType, directoryStats ? join(directoryPath, fileName) : directoryPath); - } - - private async statSafe(nodePath: string): Promise { - try { - return await stat(nodePath); - } catch { - return null; - } - } -} diff --git a/packages/node/test/node-fs.nodespec.ts b/packages/node/test/node-fs.nodespec.ts index 41f4d68c..5f329b18 100644 --- a/packages/node/test/node-fs.nodespec.ts +++ b/packages/node/test/node-fs.nodespec.ts @@ -7,8 +7,7 @@ import { platform } from "node:os"; describe("Node File System Implementation", function () { this.timeout(10_000); - const fs = createNodeFs({ watchOptions: { debounceWait: 500 } }); - const { watchService } = fs; + const fs = createNodeFs(); const testProvider = async () => { const tempDirectory = await createTempDirectory("fs-test-"); @@ -16,8 +15,6 @@ describe("Node File System Implementation", function () { return { fs, dispose: async () => { - watchService.clearGlobalListeners(); - await watchService.unwatchAllPaths(); await tempDirectory.remove(); }, tempDirectoryPath: fs.realpathSync(tempDirectory.path), diff --git a/packages/types/src/base-api-async.ts b/packages/types/src/base-api-async.ts index 6abb963d..1ce0ad42 100644 --- a/packages/types/src/base-api-async.ts +++ b/packages/types/src/base-api-async.ts @@ -7,15 +7,12 @@ import type { WriteFileOptions, } from "./common-fs-types"; import type { IFileSystemPath } from "./path"; -import type { IWatchService } from "./watch-api"; /** * ASYNC-only base file system. * Contains a subset of `fs`, watch service, and path methods. */ export interface IBaseFileSystemAsync extends IFileSystemPath { - /** @deprecated use `fs.watch()` instead. */ - watchService: IWatchService; caseSensitive: boolean; promises: IBaseFileSystemPromiseActions; diff --git a/packages/types/src/base-api-sync.ts b/packages/types/src/base-api-sync.ts index 755d6e2c..1fa28d20 100644 --- a/packages/types/src/base-api-sync.ts +++ b/packages/types/src/base-api-sync.ts @@ -10,15 +10,12 @@ import type { WriteFileOptions, } from "./common-fs-types"; import type { IFileSystemPath } from "./path"; -import type { IWatchService } from "./watch-api"; /** * SYNC-only base file system. * Contains a subset of `fs`, watch service, and path methods. */ export interface IBaseFileSystemSync extends IBaseFileSystemSyncActions, IFileSystemPath { - /** @deprecated use `fs.watch()` instead. */ - watchService: IWatchService; caseSensitive: boolean; } diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 46dfb1be..1eed4777 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -2,4 +2,3 @@ export * from "./base-api"; export * from "./common-fs-types"; export * from "./extended-api"; export * from "./path"; -export * from "./watch-api"; diff --git a/packages/types/src/watch-api.ts b/packages/types/src/watch-api.ts deleted file mode 100644 index 178f1f7d..00000000 --- a/packages/types/src/watch-api.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { IFileSystemStats } from "./common-fs-types"; - -/** - * File watching service. - * Emits naive watch events containing path and latest stats. - * @deprecated use `fs.watch()` instead. - */ -export interface IWatchService { - /** - * Start watching `path` (file or directory). - * if `listener` is provided, it will receive watch events for `path`. - * Any global listeners will also receive events for path. - */ - watchPath(path: string, listener?: WatchEventListener): Promise; - - /** - * Stop watching `path` (file or directory). - * if `listener` is provided, it will stop receiving watch events for `path`. - * if `listener is not provided, path will be unwatched with its listeners cleared. - */ - unwatchPath(path: string, listener?: WatchEventListener): Promise; - - /** - * Unwatch all watched paths. - */ - unwatchAllPaths(): Promise; - - /** - * Add a global watch event listener. - * It will receive watch events for all watched paths. - */ - addGlobalListener(listener: WatchEventListener): void; - - /** - * Remove a global watch event listener. - */ - removeGlobalListener(listener: WatchEventListener): void; - - /** - * Clears all registered global watch event listeners. - */ - clearGlobalListeners(): void; -} - -/** - * Watch event. Emitted when a file system change - * happens on a path. - */ -export interface IWatchEvent { - path: string; - stats: IFileSystemStats | null; -} - -/** - * Watch events listener function - */ -export type WatchEventListener = (watchEvent: IWatchEvent) => void; diff --git a/packages/utils/src/sync-to-async-fs.ts b/packages/utils/src/sync-to-async-fs.ts index 9763ec23..91857b71 100644 --- a/packages/utils/src/sync-to-async-fs.ts +++ b/packages/utils/src/sync-to-async-fs.ts @@ -3,7 +3,6 @@ import type { IBaseFileSystemAsync, IBaseFileSystemPromiseActions, IBaseFileSyst export function syncToAsyncFs(syncFs: IBaseFileSystemSync): IBaseFileSystemAsync { return { ...syncFs, - watchService: syncFs.watchService, caseSensitive: syncFs.caseSensitive, promises: {