Skip to content

Commit

Permalink
feat: strongly type event emitter methods
Browse files Browse the repository at this point in the history
Adds strong types to the event emitter interface such that `on` and
similar methods can infer parameters.

For example:

```ts
// these are strongly typed and will give hints in editors etc
watcher.on('ready', () => {});
watcher.on('error', (err) => {});

// this will fail
watcher.on('nonsense', () => {});
```

Fixes #1372
  • Loading branch information
43081j committed Oct 29, 2024
1 parent 97894d3 commit 7958696
Showing 1 changed file with 27 additions and 14 deletions.
41 changes: 27 additions & 14 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
EMPTY_FN,
STR_CLOSE,
STR_END,
WatchHandlers,
} from './handler.js';

type AWF = {
Expand Down Expand Up @@ -58,7 +59,8 @@ export type FSWInstanceOptions = BasicOpts & {
};

export type ThrottleType = 'readdir' | 'watch' | 'add' | 'remove' | 'change';
export type EmitArgs = [EventName, Path | Error, any?, any?, any?];
export type EmitArgs = [Path | Error, Stats?];
export type EmitArgsWithName = [EventName, ...EmitArgs];
export type MatchFunction = (val: string, stats?: Stats) => boolean;
export interface MatcherObject {
path: string;
Expand Down Expand Up @@ -295,6 +297,17 @@ export class WatchHelper {
}
}

export interface FSWatcherKnownEventMap {
[EV.READY]: [];
[EV.RAW]: Parameters<WatchHandlers['rawEmitter']>;
[EV.ERROR]: Parameters<WatchHandlers['errHandler']>;
[EV.ALL]: [EventName, ...EmitArgs];
}

export type FSWatcherEventMap = FSWatcherKnownEventMap & {
[k in Exclude<EventName, keyof FSWatcherKnownEventMap>]: EmitArgs;
};

/**
* Watches files & directories for changes. Emitted events:
* `add`, `addDir`, `change`, `unlink`, `unlinkDir`, `all`, `error`
Expand All @@ -303,7 +316,7 @@ export class WatchHelper {
* .add(directories)
* .on('add', path => log('File', path, 'was added'))
*/
export class FSWatcher extends EventEmitter {
export class FSWatcher extends EventEmitter<FSWatcherEventMap> {
closed: boolean;
options: FSWInstanceOptions;

Expand All @@ -315,13 +328,13 @@ export class FSWatcher extends EventEmitter {
_watched: Map<string, DirEntry>;

_pendingWrites: Map<string, any>;
_pendingUnlinks: Map<string, EmitArgs>;
_pendingUnlinks: Map<string, EmitArgsWithName>;
_readyCount: number;
_emitReady: () => void;
_closePromise?: Promise<void>;
_userIgnored?: MatchFunction;
_readyEmitted: boolean;
_emitRaw: () => void;
_emitRaw: WatchHandlers['rawEmitter'];
_boundRemove: (dir: string, item: string) => void;

_nodeFsHandler: NodeFsHandler;
Expand Down Expand Up @@ -567,8 +580,8 @@ export class FSWatcher extends EventEmitter {
}

emitWithAll(event: EventName, args: EmitArgs) {
this.emit(...args);
if (event !== EV.ERROR) this.emit(EV.ALL, ...args);
this.emit(event, ...args);
if (event !== EV.ERROR) this.emit(EV.ALL, event, ...args);
}

// Common helpers
Expand All @@ -588,7 +601,7 @@ export class FSWatcher extends EventEmitter {
const opts = this.options;
if (isWindows) path = sysPath.normalize(path);
if (opts.cwd) path = sysPath.relative(opts.cwd, path);
const args: EmitArgs = [event, path];
const args: EmitArgs = [path];
if (stats != null) args.push(stats);

const awf = opts.awaitWriteFinish;
Expand All @@ -600,10 +613,10 @@ export class FSWatcher extends EventEmitter {

if (opts.atomic) {
if (event === EV.UNLINK) {
this._pendingUnlinks.set(path, args);
this._pendingUnlinks.set(path, [event, ...args]);
setTimeout(
() => {
this._pendingUnlinks.forEach((entry: EmitArgs, path: Path) => {
this._pendingUnlinks.forEach((entry: EmitArgsWithName, path: Path) => {
this.emit(...entry);
this.emit(EV.ALL, ...entry);
this._pendingUnlinks.delete(path);
Expand All @@ -614,21 +627,21 @@ export class FSWatcher extends EventEmitter {
return this;
}
if (event === EV.ADD && this._pendingUnlinks.has(path)) {
event = args[0] = EV.CHANGE;
event = EV.CHANGE;
this._pendingUnlinks.delete(path);
}
}

if (awf && (event === EV.ADD || event === EV.CHANGE) && this._readyEmitted) {
const awfEmit = (err?: Error, stats?: Stats) => {
if (err) {
event = args[0] = EV.ERROR;
args[1] = err;
event = EV.ERROR;
args[0] = err;
this.emitWithAll(event, args);
} else if (stats) {
// if stats doesn't exist the file must have been deleted
if (args.length > 2) {
args[2] = stats;
if (args.length > 1) {
args[1] = stats;
} else {
args.push(stats);
}
Expand Down

0 comments on commit 7958696

Please sign in to comment.