230 lines
5.6 KiB
TypeScript
230 lines
5.6 KiB
TypeScript
/**
|
|
* Watcher builder for file system watching
|
|
*/
|
|
|
|
import type { ISmartFsProvider, IWatcherHandle } from '../interfaces/mod.provider.js';
|
|
import type { IWatchEvent, IWatchOptions, TWatchEventType } from '../interfaces/mod.types.js';
|
|
|
|
/**
|
|
* Event handler type
|
|
*/
|
|
type TEventHandler = (event: IWatchEvent) => void | Promise<void>;
|
|
|
|
/**
|
|
* Active watcher handle that allows stopping the watcher
|
|
*/
|
|
export class SmartFsActiveWatcher {
|
|
constructor(private handle: IWatcherHandle) {}
|
|
|
|
/**
|
|
* Stop watching for file system changes
|
|
*/
|
|
public async stop(): Promise<void> {
|
|
return this.handle.stop();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Watcher builder class for file system watching
|
|
* Configuration methods return `this` for chaining
|
|
* Call `.start()` to begin watching
|
|
*/
|
|
export class SmartFsWatcher {
|
|
private provider: ISmartFsProvider;
|
|
private path: string;
|
|
|
|
// Configuration options
|
|
private options: {
|
|
recursive?: boolean;
|
|
filter?: string | RegExp | ((path: string) => boolean);
|
|
debounce?: number;
|
|
} = {};
|
|
|
|
// Event handlers
|
|
private handlers: {
|
|
change?: TEventHandler[];
|
|
add?: TEventHandler[];
|
|
delete?: TEventHandler[];
|
|
all?: TEventHandler[];
|
|
} = {};
|
|
|
|
// Debounce state
|
|
private debounceTimers: Map<string, NodeJS.Timeout> = new Map();
|
|
|
|
constructor(provider: ISmartFsProvider, path: string) {
|
|
this.provider = provider;
|
|
this.path = this.provider.normalizePath(path);
|
|
|
|
if (!this.provider.capabilities.supportsWatch) {
|
|
throw new Error(`Provider '${this.provider.name}' does not support file watching`);
|
|
}
|
|
}
|
|
|
|
// --- Configuration Methods (return this for chaining) ---
|
|
|
|
/**
|
|
* Enable recursive watching (watch subdirectories)
|
|
*/
|
|
public recursive(): this {
|
|
this.options.recursive = true;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Filter watched paths
|
|
* @param filter - String pattern, RegExp, or filter function
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* // String pattern (glob-like)
|
|
* .filter('*.ts')
|
|
*
|
|
* // RegExp
|
|
* .filter(/\.ts$/)
|
|
*
|
|
* // Function
|
|
* .filter(path => path.endsWith('.ts'))
|
|
* ```
|
|
*/
|
|
public filter(filter: string | RegExp | ((path: string) => boolean)): this {
|
|
this.options.filter = filter;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Debounce events (wait N milliseconds before firing)
|
|
* Useful for avoiding rapid-fire events
|
|
* @param ms - Debounce delay in milliseconds
|
|
*/
|
|
public debounce(ms: number): this {
|
|
this.options.debounce = ms;
|
|
return this;
|
|
}
|
|
|
|
// --- Event Handler Registration (return this for chaining) ---
|
|
|
|
/**
|
|
* Register handler for 'change' events (file modified)
|
|
* @param handler - Event handler function
|
|
*/
|
|
public onChange(handler: TEventHandler): this {
|
|
if (!this.handlers.change) {
|
|
this.handlers.change = [];
|
|
}
|
|
this.handlers.change.push(handler);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Register handler for 'add' events (file created)
|
|
* @param handler - Event handler function
|
|
*/
|
|
public onAdd(handler: TEventHandler): this {
|
|
if (!this.handlers.add) {
|
|
this.handlers.add = [];
|
|
}
|
|
this.handlers.add.push(handler);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Register handler for 'delete' events (file deleted)
|
|
* @param handler - Event handler function
|
|
*/
|
|
public onDelete(handler: TEventHandler): this {
|
|
if (!this.handlers.delete) {
|
|
this.handlers.delete = [];
|
|
}
|
|
this.handlers.delete.push(handler);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Register handler for all events
|
|
* @param handler - Event handler function
|
|
*/
|
|
public onAll(handler: TEventHandler): this {
|
|
if (!this.handlers.all) {
|
|
this.handlers.all = [];
|
|
}
|
|
this.handlers.all.push(handler);
|
|
return this;
|
|
}
|
|
|
|
// --- Action Method ---
|
|
|
|
/**
|
|
* Start watching for file system changes
|
|
* @returns Active watcher handle that can be stopped
|
|
*/
|
|
public async start(): Promise<SmartFsActiveWatcher> {
|
|
const watchOptions: IWatchOptions = {
|
|
recursive: this.options.recursive,
|
|
filter: this.options.filter,
|
|
debounce: this.options.debounce,
|
|
};
|
|
|
|
// Create the callback that dispatches to handlers
|
|
const callback = async (event: IWatchEvent) => {
|
|
await this.handleEvent(event);
|
|
};
|
|
|
|
const handle = await this.provider.watch(this.path, callback, watchOptions);
|
|
return new SmartFsActiveWatcher(handle);
|
|
}
|
|
|
|
/**
|
|
* Handle incoming watch events (internal)
|
|
*/
|
|
private async handleEvent(event: IWatchEvent): Promise<void> {
|
|
// Apply debouncing if configured
|
|
if (this.options.debounce && this.options.debounce > 0) {
|
|
const key = `${event.type}:${event.path}`;
|
|
|
|
// Clear existing timer
|
|
const existingTimer = this.debounceTimers.get(key);
|
|
if (existingTimer) {
|
|
clearTimeout(existingTimer);
|
|
}
|
|
|
|
// Set new timer
|
|
const timer = setTimeout(async () => {
|
|
this.debounceTimers.delete(key);
|
|
await this.dispatchEvent(event);
|
|
}, this.options.debounce);
|
|
|
|
this.debounceTimers.set(key, timer);
|
|
} else {
|
|
// No debouncing, dispatch immediately
|
|
await this.dispatchEvent(event);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Dispatch event to registered handlers (internal)
|
|
*/
|
|
private async dispatchEvent(event: IWatchEvent): Promise<void> {
|
|
// Dispatch to type-specific handlers
|
|
const typeHandlers = this.handlers[event.type];
|
|
if (typeHandlers) {
|
|
for (const handler of typeHandlers) {
|
|
await handler(event);
|
|
}
|
|
}
|
|
|
|
// Dispatch to 'all' handlers
|
|
if (this.handlers.all) {
|
|
for (const handler of this.handlers.all) {
|
|
await handler(event);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the watched path
|
|
*/
|
|
public getPath(): string {
|
|
return this.path;
|
|
}
|
|
}
|