diff --git a/packages/cached/test/cached-fs.spec.ts b/packages/cached/test/cached-fs.spec.ts index 2a84fc3c7..f5e89ae58 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 e0e86a128..a8e1d0eeb 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 9625ec34a..924d023dc 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 5e8e8f541..f0a34b4e1 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 d2b84a117..000000000 --- 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 41f4d68ca..5f329b183 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/node/test/watch-service.nodespec.ts b/packages/node/test/watch-service.nodespec.ts deleted file mode 100644 index ef0056af1..000000000 --- a/packages/node/test/watch-service.nodespec.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { sleep } from "promise-assist"; -import { createTempDirectory, ITempDirectory } from "create-temp-directory"; -import type { IWatchService } from "@file-services/types"; -import { WatchEventsValidator } from "@file-services/test-kit"; -import { NodeWatchService, nodeFs } from "@file-services/node"; - -const { writeFile, stat, mkdir, rmdir } = nodeFs.promises; - -const debounceWait = 500; -const SAMPLE_CONTENT = `sample file content`; - -describe("Node Watch Service", function () { - this.timeout(10_000); - - let tempDir: ITempDirectory; - let watchService: IWatchService; - - afterEach("delete temp directory and close watch service", async () => { - watchService.clearGlobalListeners(); - await watchService.unwatchAllPaths(); - await tempDir.remove(); - }); - - describe("watching files", () => { - let validator: WatchEventsValidator; - let testFilePath: string; - - beforeEach("create temp fixture file and intialize watch service", async () => { - watchService = new NodeWatchService({ debounceWait }); - validator = new WatchEventsValidator(watchService); - - tempDir = await createTempDirectory(); - testFilePath = nodeFs.join(tempDir.path, "test-file"); - - await writeFile(testFilePath, SAMPLE_CONTENT); - await watchService.watchPath(testFilePath); - }); - - it("debounces several consecutive watch events as a single watch event", async () => { - await writeFile(testFilePath, SAMPLE_CONTENT); - await writeFile(testFilePath, SAMPLE_CONTENT); - await writeFile(testFilePath, SAMPLE_CONTENT); - - await validator.validateEvents(async () => [{ path: testFilePath, stats: await stat(testFilePath) }]); - await validator.noMoreEvents(); - }); - - it(`emits two different watch events when changes are >${debounceWait}ms appart`, async () => { - await writeFile(testFilePath, SAMPLE_CONTENT); - - await sleep(debounceWait * 1.1); - - const firstWriteStats = await stat(testFilePath); - - await writeFile(testFilePath, SAMPLE_CONTENT); - - await validator.validateEvents(async () => [ - { path: testFilePath, stats: firstWriteStats }, - { path: testFilePath, stats: await stat(testFilePath) }, - ]); - await validator.noMoreEvents(); - }); - - it(`does not emit events after unwatching a path`, async () => { - await writeFile(testFilePath, SAMPLE_CONTENT); - await watchService.unwatchPath(testFilePath); - - await validator.noMoreEvents(); - }); - - it(`does not emit events after unwatching all path`, async () => { - await writeFile(testFilePath, SAMPLE_CONTENT); - await watchService.unwatchAllPaths(); - - await validator.noMoreEvents(); - }); - }); - - describe("watching directories", () => { - let validator: WatchEventsValidator; - let testDirectoryPath: string; - - beforeEach("create temp fixture directory and intialize watch service", async () => { - watchService = new NodeWatchService({ debounceWait }); - validator = new WatchEventsValidator(watchService); - - tempDir = await createTempDirectory(); - testDirectoryPath = nodeFs.join(tempDir.path, "test-directory"); - await mkdir(testDirectoryPath); - }); - - it("fires a watch event when a watched directory is removed", async () => { - await watchService.watchPath(tempDir.path); - - await rmdir(testDirectoryPath); - - await validator.validateEvents([{ path: testDirectoryPath, stats: null }]); - await validator.noMoreEvents(); - }); - - it("unwatchAllPaths does not get stuck when invoked immediately after watched dir is removed", async () => { - await watchService.watchPath(testDirectoryPath); - await rmdir(testDirectoryPath); - await watchService.unwatchAllPaths(); - }); - }); - - describe("mixing watch of directories and files", () => { - let validator: WatchEventsValidator; - let testDirectoryPath: string; - let testFilePath: string; - - beforeEach("create temp fixture directory and intialize watch service", async () => { - watchService = new NodeWatchService({ debounceWait }); - validator = new WatchEventsValidator(watchService); - - tempDir = await createTempDirectory(); - testDirectoryPath = nodeFs.join(tempDir.path, "test-directory"); - await mkdir(testDirectoryPath); - testFilePath = nodeFs.join(testDirectoryPath, "test-file"); - await writeFile(testFilePath, SAMPLE_CONTENT); - }); - - it("allows watching a file and its containing directory", async () => { - await watchService.watchPath(testFilePath); - await watchService.watchPath(testDirectoryPath); - - await writeFile(testFilePath, SAMPLE_CONTENT); - - await validator.validateEvents(async () => [{ path: testFilePath, stats: await stat(testFilePath) }]); - await validator.noMoreEvents(); - }); - - it("allows watching in any order", async () => { - await watchService.watchPath(testDirectoryPath); - await watchService.watchPath(testFilePath); - - await writeFile(testFilePath, SAMPLE_CONTENT); - - await validator.validateEvents(async () => [{ path: testFilePath, stats: await stat(testFilePath) }]); - await validator.noMoreEvents(); - }); - }); -}); diff --git a/packages/test-kit/README.md b/packages/test-kit/README.md index 69c3e00c5..aacb4d0a9 100644 --- a/packages/test-kit/README.md +++ b/packages/test-kit/README.md @@ -4,7 +4,6 @@ File system test-kit, containing contracts and validators. -- WatchEventsValidator - asyncBaseFsContract - syncBaseFsContract - asyncFsContract diff --git a/packages/test-kit/src/async-base-fs-contract.ts b/packages/test-kit/src/async-base-fs-contract.ts index ad92a18b2..0f11e3f2b 100644 --- a/packages/test-kit/src/async-base-fs-contract.ts +++ b/packages/test-kit/src/async-base-fs-contract.ts @@ -2,7 +2,6 @@ import chai, { expect } from "chai"; import chaiAsPromised from "chai-as-promised"; import { IBaseFileSystemAsync, FileSystemConstants } from "@file-services/types"; import type { ITestInput } from "./types"; -import { WatchEventsValidator } from "./watch-events-validator"; chai.use(chaiAsPromised); @@ -201,75 +200,6 @@ export function asyncBaseFsContract(testProvider: () => Promise { - const { - tempDirectoryPath, - fs: { - join, - promises: { writeFile }, - watchService, - }, - } = testInput; - validator = new WatchEventsValidator(watchService); - - testFilePath = join(tempDirectoryPath, "test-file"); - - await writeFile(testFilePath, SAMPLE_CONTENT); - await watchService.watchPath(testFilePath); - }); - - it("emits watch event when a watched file changes", async () => { - const { - fs: { - promises: { writeFile, stat }, - }, - } = testInput; - - await writeFile(testFilePath, DIFFERENT_CONTENT); - - await validator.validateEvents([{ path: testFilePath, stats: await stat(testFilePath) }]); - await validator.noMoreEvents(); - }); - - it("emits watch event when a watched file is removed", async () => { - const { - fs: { - promises: { unlink }, - }, - } = testInput; - - await unlink(testFilePath); - - await validator.validateEvents([{ path: testFilePath, stats: null }]); - await validator.noMoreEvents(); - }); - - it("keeps watching if file is deleted and recreated immediately", async () => { - const { - fs: { - promises: { unlink, writeFile, stat }, - }, - } = testInput; - - await writeFile(testFilePath, SAMPLE_CONTENT); - await unlink(testFilePath); - await writeFile(testFilePath, SAMPLE_CONTENT); - - await validator.validateEvents([{ path: testFilePath, stats: await stat(testFilePath) }]); - - await writeFile(testFilePath, SAMPLE_CONTENT); - - await validator.validateEvents([{ path: testFilePath, stats: await stat(testFilePath) }]); - await validator.noMoreEvents(); - }); - }); - describe("creating directories", () => { it("can create an empty directory inside an existing one", async () => { const { @@ -528,143 +458,6 @@ export function asyncBaseFsContract(testProvider: () => Promise { - const { - tempDirectoryPath, - fs: { - promises: { mkdir }, - join, - watchService, - }, - } = testInput; - validator = new WatchEventsValidator(watchService); - - testDirectoryPath = join(tempDirectoryPath, "test-directory"); - await mkdir(testDirectoryPath); - }); - - it("fires a watch event when a file is added inside a watched directory", async () => { - const { - fs: { - promises: { writeFile, stat }, - join, - watchService, - }, - } = testInput; - - await watchService.watchPath(testDirectoryPath); - - const testFilePath = join(testDirectoryPath, "test-file"); - await writeFile(testFilePath, SAMPLE_CONTENT); - - await validator.validateEvents([{ path: testFilePath, stats: await stat(testFilePath) }]); - await validator.noMoreEvents(); - }); - - it("fires a watch event when a file is changed inside a watched directory", async () => { - const { - fs: { - promises: { writeFile, stat }, - join, - watchService, - }, - } = testInput; - - const testFilePath = join(testDirectoryPath, "test-file"); - await writeFile(testFilePath, SAMPLE_CONTENT); - await watchService.watchPath(testDirectoryPath); - - await writeFile(testFilePath, SAMPLE_CONTENT); - - await validator.validateEvents([{ path: testFilePath, stats: await stat(testFilePath) }]); - await validator.noMoreEvents(); - }); - - it("fires a watch event when a file is removed inside a watched directory", async () => { - const { - fs: { - promises: { writeFile, unlink }, - join, - watchService, - }, - } = testInput; - - const testFilePath = join(testDirectoryPath, "test-file"); - await writeFile(testFilePath, SAMPLE_CONTENT); - await watchService.watchPath(testDirectoryPath); - - await unlink(testFilePath); - - await validator.validateEvents([{ path: testFilePath, stats: null }]); - await validator.noMoreEvents(); - }); - }); - - describe("watching both directories and files", function () { - this.timeout(10000); - - let validator: WatchEventsValidator; - let testDirectoryPath: string; - let testFilePath: string; - - beforeEach("create temp fixture directory and intialize watch service", async () => { - const { - tempDirectoryPath, - fs: { - promises: { writeFile, mkdir }, - join, - watchService, - }, - } = testInput; - validator = new WatchEventsValidator(watchService); - - testDirectoryPath = join(tempDirectoryPath, "test-directory"); - await mkdir(testDirectoryPath); - testFilePath = join(testDirectoryPath, "test-file"); - await writeFile(testFilePath, SAMPLE_CONTENT); - }); - - it("allows watching a file and its containing directory", async () => { - const { - fs: { - promises: { writeFile, stat }, - watchService, - }, - } = testInput; - - await watchService.watchPath(testFilePath); - await watchService.watchPath(testDirectoryPath); - - await writeFile(testFilePath, SAMPLE_CONTENT); - - await validator.validateEvents([{ path: testFilePath, stats: await stat(testFilePath) }]); - await validator.noMoreEvents(); - }); - - it("allows watching in any order", async () => { - const { - fs: { - promises: { writeFile, stat }, - watchService, - }, - } = testInput; - - await watchService.watchPath(testDirectoryPath); - await watchService.watchPath(testFilePath); - - await writeFile(testFilePath, SAMPLE_CONTENT); - - await validator.validateEvents([{ path: testFilePath, stats: await stat(testFilePath) }]); - await validator.noMoreEvents(); - }); - }); - describe("renaming directories and files", () => { it("moves a file", async () => { const { diff --git a/packages/test-kit/src/index.ts b/packages/test-kit/src/index.ts index be4e1e010..f70b7876f 100644 --- a/packages/test-kit/src/index.ts +++ b/packages/test-kit/src/index.ts @@ -1,5 +1,4 @@ export * from "./types"; -export * from "./watch-events-validator"; export * from "./async-base-fs-contract"; export * from "./sync-base-fs-contract"; diff --git a/packages/test-kit/src/sync-base-fs-contract.ts b/packages/test-kit/src/sync-base-fs-contract.ts index 259986558..f07cd785c 100644 --- a/packages/test-kit/src/sync-base-fs-contract.ts +++ b/packages/test-kit/src/sync-base-fs-contract.ts @@ -2,7 +2,6 @@ import { FileSystemConstants, type FSWatcher, type IBaseFileSystemSync } from "@ import { expect } from "chai"; import { sleep, waitFor } from "promise-assist"; import type { ITestInput } from "./types"; -import { WatchEventsValidator } from "./watch-events-validator"; const SAMPLE_CONTENT = "content"; const DIFFERENT_CONTENT = "another content"; @@ -271,57 +270,6 @@ export function syncBaseFsContract( }); }); - describe("watching files (using WatchService)", function () { - this.timeout(10000); - - let validator: WatchEventsValidator; - let testFilePath: string; - - beforeEach("create temp fixture file and intialize validator", async () => { - const { fs, tempDirectoryPath } = testInput; - const { watchService } = fs; - validator = new WatchEventsValidator(watchService); - - testFilePath = fs.join(tempDirectoryPath, "test-file"); - - fs.writeFileSync(testFilePath, SAMPLE_CONTENT); - await watchService.watchPath(testFilePath); - }); - - it("emits watch event when a watched file changes", async () => { - const { fs } = testInput; - - fs.writeFileSync(testFilePath, DIFFERENT_CONTENT); - - await validator.validateEvents([{ path: testFilePath, stats: fs.statSync(testFilePath) }]); - await validator.noMoreEvents(); - }); - - it("emits watch event when a watched file is removed", async () => { - const { fs } = testInput; - - fs.unlinkSync(testFilePath); - - await validator.validateEvents([{ path: testFilePath, stats: null }]); - await validator.noMoreEvents(); - }); - - it("keeps watching if file is deleted and recreated immediately", async () => { - const { fs } = testInput; - - fs.writeFileSync(testFilePath, SAMPLE_CONTENT); - fs.unlinkSync(testFilePath); - fs.writeFileSync(testFilePath, SAMPLE_CONTENT); - - await validator.validateEvents([{ path: testFilePath, stats: fs.statSync(testFilePath) }]); - - fs.writeFileSync(testFilePath, SAMPLE_CONTENT); - - await validator.validateEvents([{ path: testFilePath, stats: fs.statSync(testFilePath) }]); - await validator.noMoreEvents(); - }); - }); - describe("creating directories", () => { it("can create an empty directory inside an existing one", () => { const { fs, tempDirectoryPath } = testInput; @@ -529,103 +477,6 @@ export function syncBaseFsContract( }); }); - describe("watching directories", function () { - this.timeout(10000); - - let validator: WatchEventsValidator; - let testDirectoryPath: string; - - beforeEach("create temp fixture directory and intialize validator", async () => { - const { fs, tempDirectoryPath } = testInput; - validator = new WatchEventsValidator(fs.watchService); - - testDirectoryPath = fs.join(tempDirectoryPath, "test-directory"); - fs.mkdirSync(testDirectoryPath); - }); - - it("fires a watch event when a file is added inside a watched directory", async () => { - const { fs } = testInput; - - await fs.watchService.watchPath(testDirectoryPath); - - const testFilePath = fs.join(testDirectoryPath, "test-file"); - fs.writeFileSync(testFilePath, SAMPLE_CONTENT); - - await validator.validateEvents([{ path: testFilePath, stats: fs.statSync(testFilePath) }]); - await validator.noMoreEvents(); - }); - - it("fires a watch event when a file is changed inside a watched directory", async () => { - const { fs } = testInput; - - const testFilePath = fs.join(testDirectoryPath, "test-file"); - fs.writeFileSync(testFilePath, SAMPLE_CONTENT); - await fs.watchService.watchPath(testDirectoryPath); - - fs.writeFileSync(testFilePath, SAMPLE_CONTENT); - - await validator.validateEvents([{ path: testFilePath, stats: fs.statSync(testFilePath) }]); - await validator.noMoreEvents(); - }); - - it("fires a watch event when a file is removed inside a watched directory", async () => { - const { fs } = testInput; - - const testFilePath = fs.join(testDirectoryPath, "test-file"); - fs.writeFileSync(testFilePath, SAMPLE_CONTENT); - await fs.watchService.watchPath(testDirectoryPath); - - fs.unlinkSync(testFilePath); - - await validator.validateEvents([{ path: testFilePath, stats: null }]); - await validator.noMoreEvents(); - }); - }); - - describe("watching both directories and files", function () { - this.timeout(10000); - - let validator: WatchEventsValidator; - let testDirectoryPath: string; - let testFilePath: string; - - beforeEach("create temp fixture directory and intialize watchService", async () => { - const { fs, tempDirectoryPath } = testInput; - validator = new WatchEventsValidator(fs.watchService); - - testDirectoryPath = fs.join(tempDirectoryPath, "test-directory"); - fs.mkdirSync(testDirectoryPath); - testFilePath = fs.join(testDirectoryPath, "test-file"); - fs.writeFileSync(testFilePath, SAMPLE_CONTENT); - }); - - it("allows watching a file and its containing directory", async () => { - const { fs } = testInput; - const { watchService } = fs; - - await watchService.watchPath(testFilePath); - await watchService.watchPath(testDirectoryPath); - - fs.writeFileSync(testFilePath, SAMPLE_CONTENT); - - await validator.validateEvents([{ path: testFilePath, stats: fs.statSync(testFilePath) }]); - await validator.noMoreEvents(); - }); - - it("allows watching in any order", async () => { - const { fs } = testInput; - const { watchService } = fs; - - await watchService.watchPath(testDirectoryPath); - await watchService.watchPath(testFilePath); - - fs.writeFileSync(testFilePath, SAMPLE_CONTENT); - - await validator.validateEvents([{ path: testFilePath, stats: fs.statSync(testFilePath) }]); - await validator.noMoreEvents(); - }); - }); - describe("renaming directories and files", () => { it("moves a file", () => { const { fs, tempDirectoryPath } = testInput; diff --git a/packages/test-kit/src/watch-events-validator.ts b/packages/test-kit/src/watch-events-validator.ts deleted file mode 100644 index aa7c296fa..000000000 --- a/packages/test-kit/src/watch-events-validator.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { expect } from "chai"; -import { sleep, waitFor } from "promise-assist"; -import type { IWatchEvent, IWatchService } from "@file-services/types"; - -export interface IWatchEventValidatorOptions { - /** - * Timeout (in ms) for each `validateEvents` call. - * - * @default 5000 - */ - validateTimeout?: number; - - /** - * Timeout (in ms) to wait before checking for no additional events. - * - * @default 500 - */ - noMoreEventsTimeout?: number; -} - -export class WatchEventsValidator { - // actual captured events from watch service - private capturedEvents: IWatchEvent[] = []; - - // resolved options (defaults + user overrides) - private options: Required; - - constructor( - private watchService: IWatchService, - options?: IWatchEventValidatorOptions, - ) { - this.options = { validateTimeout: 5000, noMoreEventsTimeout: 500, ...options }; - - this.watchService.addGlobalListener((e) => this.capturedEvents.push(e)); - } - - /** - * Resolves or rejects depending whether last captured watch events - * equal `expectedEvents` - */ - public async validateEvents( - expectedEventsProvider: IWatchEvent[] | (() => IWatchEvent[] | Promise), - ): Promise { - const { capturedEvents } = this; - - await waitFor( - async () => { - const expectedEvents = - typeof expectedEventsProvider === "function" ? await expectedEventsProvider() : expectedEventsProvider; - const expected = expectedEvents.map(simplifyEvent); - const actual = capturedEvents.slice(-1 * expectedEvents.length).map(simplifyEvent); - expect(actual).to.eql(expected); - }, - { timeout: this.options.validateTimeout, delay: 100 }, - ); - - this.capturedEvents.length = 0; - } - - /** - * Assert no additional watch events came in, expect for validated ones. - */ - public async noMoreEvents(): Promise { - await sleep(this.options.noMoreEventsTimeout); - expect(this.capturedEvents.map(simplifyEvent)).to.eql([]); - } -} - -/** - * Converts watch event's stats to an easier to read/diff structure - */ -const simplifyEvent = ({ path, stats }: IWatchEvent) => ({ - path, - stats: stats ? { birthtime: stats.birthtime.getTime(), mtime: stats.mtime.getTime() } : null, -}); diff --git a/packages/types/src/base-api-async.ts b/packages/types/src/base-api-async.ts index 6abb963d8..1ce0ad42e 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 755d6e2c0..1fa28d20e 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 46dfb1be4..1eed47774 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 178f1f7d1..000000000 --- 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 9763ec23c..91857b710 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: {