Files
smartfs/ts/classes/smartfs.watcher.ts
2025-11-21 18:36:31 +00:00

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;
}
}