From 208af3b9070780d9aaf55adab045561db95e5e08 Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Fri, 30 Jan 2026 01:01:10 +0000 Subject: [PATCH] Add utility modules --- src/utils/watcher.ts | 141 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 src/utils/watcher.ts diff --git a/src/utils/watcher.ts b/src/utils/watcher.ts new file mode 100644 index 0000000..3bafe5a --- /dev/null +++ b/src/utils/watcher.ts @@ -0,0 +1,141 @@ +import chokidar, { FSWatcher, WatchOptions } from 'chokidar'; +import * as path from 'path'; + +export interface WatcherOptions extends WatchOptions { + debounceMs?: number; + maxRetries?: number; +} + +export interface FileChangeEvent { + type: 'add' | 'change' | 'unlink'; + path: string; + timestamp: Date; +} + +export type WatcherEventHandler = (event: FileChangeEvent) => void; + +export class FileWatcher { + private watcher: FSWatcher | null = null; + private handlers: Map> = new Map(); + private options: WatcherOptions; + private isRunning: boolean = false; + + constructor(options: WatcherOptions = {}) { + this.options = { + debounceMs: options.debounceMs ?? 100, + maxRetries: options.maxRetries ?? 3, + persistent: options.persistent ?? true, + ignoreInitial: options.ignoreInitial ?? false, + awaitWriteFinish: options.awaitWriteFinish ?? { + stabilityThreshold: 100, + pollInterval: 50 + }, + ...options + }; + } + + watch(paths: string | string[], patterns?: string[]): Promise { + return new Promise((resolve, reject) => { + const watchPaths = Array.isArray(paths) ? paths : [paths]; + + this.watcher = chokidar.watch(watchPaths, { + ...this.options, + ignored: patterns + }); + + this.watcher + .on('ready', () => { + this.isRunning = true; + resolve(); + }) + .on('error', (error) => { + this.isRunning = false; + reject(error); + }) + .on('add', (filePath) => { + this.emit('add', filePath); + }) + .on('change', (filePath) => { + this.emit('change', filePath); + }) + .on('unlink', (filePath) => { + this.emit('unlink', filePath); + }); + }); + } + + on(eventType: string, handler: WatcherEventHandler): void { + if (!this.handlers.has(eventType)) { + this.handlers.set(eventType, new Set()); + } + this.handlers.get(eventType)!.add(handler); + } + + off(eventType: string, handler: WatcherEventHandler): void { + const eventHandlers = this.handlers.get(eventType); + if (eventHandlers) { + eventHandlers.delete(handler); + } + } + + private emit(eventType: string, filePath: string): void { + const event: FileChangeEvent = { + type: eventType as 'add' | 'change' | 'unlink', + path: path.resolve(filePath), + timestamp: new Date() + }; + + const handlers = this.handlers.get(eventType); + if (handlers) { + for (const handler of handlers) { + try { + handler(event); + } catch (error) { + console.error(`Error in watcher handler for ${eventType}:`, error); + } + } + } + + const allHandlers = this.handlers.get('*'); + if (allHandlers) { + for (const handler of allHandlers) { + try { + handler(event); + } catch (error) { + console.error('Error in wildcard watcher handler:', error); + } + } + } + } + + getWatchedFiles(): string[] { + if (!this.watcher) { + return []; + } + return Object.values(this.watcher.getWatched()).flat(); + } + + add(filePath: string): void { + this.watcher?.add(filePath); + } + + unwatch(filePath: string): void { + this.watcher?.unwatch(filePath); + } + + close(): void { + if (this.watcher) { + this.watcher.close(); + this.watcher = null; + this.isRunning = false; + } + } + + isActive(): boolean { + return this.isRunning; + } +} + +export function createFileWatcher(options?: WatcherOptions): FileWatcher { + return new FileWatcher(options); +}