Skip to content

Commit c829803

Browse files
alienzhou周鸿轩
andauthored
feat: support the recursive option for fs.watch() (#902)
Co-authored-by: 周鸿轩 <[email protected]>
1 parent 0cc6ec1 commit c829803

File tree

2 files changed

+149
-9
lines changed

2 files changed

+149
-9
lines changed

src/__tests__/volume.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1080,6 +1080,75 @@ describe('volume', () => {
10801080
});
10811081
});
10821082
});
1083+
describe('.watch(path[, options], listener)', () => {
1084+
it('Calls listener on .watch when renaming with recursive=true', done => {
1085+
const vol = new Volume();
1086+
vol.mkdirSync('/test');
1087+
vol.writeFileSync('/test/lol.txt', 'foo');
1088+
setTimeout(() => {
1089+
const listener = jest.fn();
1090+
const watcher = vol.watch('/', { recursive: true }, listener);
1091+
1092+
vol.renameSync('/test/lol.txt', '/test/lol-2.txt');
1093+
1094+
setTimeout(() => {
1095+
watcher.close();
1096+
expect(listener).toBeCalledTimes(2);
1097+
expect(listener).nthCalledWith(1, 'rename', 'test/lol.txt');
1098+
expect(listener).nthCalledWith(2, 'rename', 'test/lol-2.txt');
1099+
done();
1100+
}, 10);
1101+
});
1102+
});
1103+
it('Calls listener on .watch with recursive=true', done => {
1104+
const vol = new Volume();
1105+
vol.writeFileSync('/lol.txt', '1');
1106+
vol.mkdirSync('/test');
1107+
setTimeout(() => {
1108+
const listener = jest.fn();
1109+
const watcher = vol.watch('/', { recursive: true }, listener);
1110+
vol.writeFileSync('/lol.txt', '2');
1111+
vol.writeFileSync('/test/lol.txt', '2');
1112+
vol.rmSync('/lol.txt');
1113+
vol.rmSync('/test/lol.txt');
1114+
vol.mkdirSync('/test/foo');
1115+
1116+
setTimeout(() => {
1117+
watcher.close();
1118+
expect(listener).toBeCalledTimes(6);
1119+
expect(listener).nthCalledWith(1, 'change', 'lol.txt');
1120+
expect(listener).nthCalledWith(2, 'change', 'lol.txt');
1121+
expect(listener).nthCalledWith(3, 'rename', 'test/lol.txt');
1122+
expect(listener).nthCalledWith(4, 'rename', 'lol.txt');
1123+
expect(listener).nthCalledWith(5, 'rename', 'test/lol.txt');
1124+
expect(listener).nthCalledWith(6, 'rename', 'test/foo');
1125+
done();
1126+
}, 10);
1127+
});
1128+
});
1129+
it('Calls listener on .watch with recursive=false', done => {
1130+
const vol = new Volume();
1131+
vol.writeFileSync('/lol.txt', '1');
1132+
vol.mkdirSync('/test');
1133+
setTimeout(() => {
1134+
const listener = jest.fn();
1135+
const watcher = vol.watch('/', { recursive: false }, listener);
1136+
vol.writeFileSync('/lol.txt', '2');
1137+
vol.rmSync('/lol.txt');
1138+
vol.writeFileSync('/test/lol.txt', '2');
1139+
vol.rmSync('/test/lol.txt');
1140+
1141+
setTimeout(() => {
1142+
watcher.close();
1143+
expect(listener).toBeCalledTimes(3);
1144+
expect(listener).nthCalledWith(1, 'change', 'lol.txt');
1145+
expect(listener).nthCalledWith(2, 'change', 'lol.txt');
1146+
expect(listener).nthCalledWith(3, 'rename', 'lol.txt');
1147+
done();
1148+
}, 10);
1149+
});
1150+
});
1151+
});
10831152
describe('.watchFile(path[, options], listener)', () => {
10841153
it('Calls listener on .writeFile', done => {
10851154
const vol = new Volume();

src/volume.ts

Lines changed: 80 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ function optsGenerator<TOpts>(defaults: TOpts): (opts) => TOpts {
230230
return options => getOptions(defaults, options);
231231
}
232232

233-
type AssertCallback<T> = T extends Function ? T : never;
233+
type AssertCallback<T> = T extends () => void ? T : never;
234234

235235
function validateCallback<T>(callback: T): AssertCallback<T> {
236236
if (typeof callback !== 'function') throw TypeError(ERRSTR.CB);
@@ -2688,6 +2688,9 @@ export class FSWatcher extends EventEmitter {
26882688

26892689
_timer; // Timer that keeps this task persistent.
26902690

2691+
// inode -> removers
2692+
private _listenerRemovers = new Map<number, Array<() => void>>();
2693+
26912694
constructor(vol: Volume) {
26922695
super();
26932696
this._vol = vol;
@@ -2711,10 +2714,6 @@ export class FSWatcher extends EventEmitter {
27112714
return this._steps[this._steps.length - 1];
27122715
}
27132716

2714-
private _onNodeChange = () => {
2715-
this._emit('change');
2716-
};
2717-
27182717
private _onParentChild = (link: Link) => {
27192718
if (link.getName() === this._getName()) {
27202719
this._emit('rename');
@@ -2751,10 +2750,79 @@ export class FSWatcher extends EventEmitter {
27512750
throw error;
27522751
}
27532752

2754-
this._link.getNode().on('change', this._onNodeChange);
2753+
const watchLinkNodeChanged = (link: Link) => {
2754+
const filepath = link.getPath();
2755+
const node = link.getNode();
2756+
const onNodeChange = () => this.emit('change', 'change', relative(this._filename, filepath));
2757+
node.on('change', onNodeChange);
2758+
2759+
const removers = this._listenerRemovers.get(node.ino) ?? [];
2760+
removers.push(() => node.removeListener('change', onNodeChange));
2761+
this._listenerRemovers.set(node.ino, removers);
2762+
};
2763+
2764+
const watchLinkChildrenChanged = (link: Link) => {
2765+
const node = link.getNode();
2766+
2767+
// when a new link added
2768+
const onLinkChildAdd = (l: Link) => {
2769+
this.emit('change', 'rename', relative(this._filename, l.getPath()));
27552770

2756-
this._link.on('child:add', this._onNodeChange);
2757-
this._link.on('child:delete', this._onNodeChange);
2771+
setTimeout(() => {
2772+
// 1. watch changes of the new link-node
2773+
watchLinkNodeChanged(l);
2774+
// 2. watch changes of the new link-node's children
2775+
watchLinkChildrenChanged(l);
2776+
});
2777+
};
2778+
2779+
// when a new link deleted
2780+
const onLinkChildDelete = (l: Link) => {
2781+
// remove the listeners of the children nodes
2782+
const removeLinkNodeListeners = (curLink: Link) => {
2783+
const ino = curLink.getNode().ino;
2784+
const removers = this._listenerRemovers.get(ino);
2785+
if (removers) {
2786+
removers.forEach(r => r());
2787+
this._listenerRemovers.delete(ino);
2788+
}
2789+
Object.values(curLink.children).forEach(childLink => {
2790+
if (childLink) {
2791+
removeLinkNodeListeners(childLink);
2792+
}
2793+
});
2794+
};
2795+
removeLinkNodeListeners(l);
2796+
2797+
this.emit('change', 'rename', relative(this._filename, l.getPath()));
2798+
};
2799+
2800+
// children nodes changed
2801+
Object.values(link.children).forEach(childLink => {
2802+
if (childLink) {
2803+
watchLinkNodeChanged(childLink);
2804+
}
2805+
});
2806+
// link children add/remove
2807+
link.on('child:add', onLinkChildAdd);
2808+
link.on('child:delete', onLinkChildDelete);
2809+
2810+
const removers = this._listenerRemovers.get(node.ino) ?? [];
2811+
removers.push(() => {
2812+
link.removeListener('child:add', onLinkChildAdd);
2813+
link.removeListener('child:delete', onLinkChildDelete);
2814+
});
2815+
2816+
if (recursive) {
2817+
Object.values(link.children).forEach(childLink => {
2818+
if (childLink) {
2819+
watchLinkChildrenChanged(childLink);
2820+
}
2821+
});
2822+
}
2823+
};
2824+
watchLinkNodeChanged(this._link);
2825+
watchLinkChildrenChanged(this._link);
27582826

27592827
const parent = this._link.parent;
27602828
if (parent) {
@@ -2769,7 +2837,10 @@ export class FSWatcher extends EventEmitter {
27692837
close() {
27702838
clearTimeout(this._timer);
27712839

2772-
this._link.getNode().removeListener('change', this._onNodeChange);
2840+
this._listenerRemovers.forEach(removers => {
2841+
removers.forEach(r => r());
2842+
});
2843+
this._listenerRemovers.clear();
27732844

27742845
const parent = this._link.parent;
27752846
if (parent) {

0 commit comments

Comments
 (0)