Skip to content

Commit

Permalink
feat(volume): support the recursive option for fs.watch()
Browse files Browse the repository at this point in the history
  • Loading branch information
周鸿轩 authored and 周鸿轩 committed Apr 6, 2023
1 parent 0cc6ec1 commit 5e7c1dc
Show file tree
Hide file tree
Showing 2 changed files with 149 additions and 9 deletions.
69 changes: 69 additions & 0 deletions src/__tests__/volume.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1080,6 +1080,75 @@ describe('volume', () => {
});
});
});
describe('.watch(path[, options], listener)', () => {
it('Calls listener on .watch when renaming with recursive=true', done => {
const vol = new Volume();
vol.mkdirSync('/test');
vol.writeFileSync('/test/lol.txt', 'foo');
setTimeout(() => {
const listener = jest.fn();
const watcher = vol.watch('/', { recursive: true }, listener);

vol.renameSync('/test/lol.txt', '/test/lol-2.txt');

setTimeout(() => {
watcher.close();
expect(listener).toBeCalledTimes(2);
expect(listener).nthCalledWith(1, 'rename', 'test/lol.txt');
expect(listener).nthCalledWith(2, 'rename', 'test/lol-2.txt');
done();
}, 10);
});
});
it('Calls listener on .watch with recursive=true', done => {
const vol = new Volume();
vol.writeFileSync('/lol.txt', '1');
vol.mkdirSync('/test');
setTimeout(() => {
const listener = jest.fn();
const watcher = vol.watch('/', { recursive: true }, listener);
vol.writeFileSync('/lol.txt', '2');
vol.writeFileSync('/test/lol.txt', '2');
vol.rmSync('/lol.txt');
vol.rmSync('/test/lol.txt');
vol.mkdirSync('/test/foo');

setTimeout(() => {
watcher.close();
expect(listener).toBeCalledTimes(6);
expect(listener).nthCalledWith(1, 'change', 'lol.txt');
expect(listener).nthCalledWith(2, 'change', 'lol.txt');
expect(listener).nthCalledWith(3, 'rename', 'test/lol.txt');
expect(listener).nthCalledWith(4, 'rename', 'lol.txt');
expect(listener).nthCalledWith(5, 'rename', 'test/lol.txt');
expect(listener).nthCalledWith(6, 'rename', 'test/foo');
done();
}, 10);
});
});
it('Calls listener on .watch with recursive=false', done => {
const vol = new Volume();
vol.writeFileSync('/lol.txt', '1');
vol.mkdirSync('/test');
setTimeout(() => {
const listener = jest.fn();
const watcher = vol.watch('/', { recursive: false }, listener);
vol.writeFileSync('/lol.txt', '2');
vol.rmSync('/lol.txt');
vol.writeFileSync('/test/lol.txt', '2');
vol.rmSync('/test/lol.txt');

setTimeout(() => {
watcher.close();
expect(listener).toBeCalledTimes(3);
expect(listener).nthCalledWith(1, 'change', 'lol.txt');
expect(listener).nthCalledWith(2, 'change', 'lol.txt');
expect(listener).nthCalledWith(3, 'rename', 'lol.txt');
done();
}, 10);
});
});
});
describe('.watchFile(path[, options], listener)', () => {
it('Calls listener on .writeFile', done => {
const vol = new Volume();
Expand Down
89 changes: 80 additions & 9 deletions src/volume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ function optsGenerator<TOpts>(defaults: TOpts): (opts) => TOpts {
return options => getOptions(defaults, options);
}

type AssertCallback<T> = T extends Function ? T : never;
type AssertCallback<T> = T extends () => void ? T : never;

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

_timer; // Timer that keeps this task persistent.

// inode -> removers
private _listenerRemovers = new Map<number, Array<() => void>>();

constructor(vol: Volume) {
super();
this._vol = vol;
Expand All @@ -2711,10 +2714,6 @@ export class FSWatcher extends EventEmitter {
return this._steps[this._steps.length - 1];
}

private _onNodeChange = () => {
this._emit('change');
};

private _onParentChild = (link: Link) => {
if (link.getName() === this._getName()) {
this._emit('rename');
Expand Down Expand Up @@ -2751,10 +2750,79 @@ export class FSWatcher extends EventEmitter {
throw error;
}

this._link.getNode().on('change', this._onNodeChange);
const watchLinkNodeChanged = (link: Link) => {
const filepath = link.getPath();
const node = link.getNode();
const onNodeChange = () => this.emit('change', 'change', relative(this._filename, filepath));
node.on('change', onNodeChange);

const removers = this._listenerRemovers.get(node.ino) ?? [];
removers.push(() => node.removeListener('change', onNodeChange));
this._listenerRemovers.set(node.ino, removers);
};

const watchLinkChildrenChanged = (link: Link) => {
const node = link.getNode();

// when a new link added
const onLinkChildAdd = (l: Link) => {
this.emit('change', 'rename', relative(this._filename, l.getPath()));

this._link.on('child:add', this._onNodeChange);
this._link.on('child:delete', this._onNodeChange);
setTimeout(() => {
// 1. watch changes of the new link-node
watchLinkNodeChanged(l);
// 2. watch changes of the new link-node's children
watchLinkChildrenChanged(l);
});
};

// when a new link deleted
const onLinkChildDelete = (l: Link) => {
// remove the listeners of the children nodes
const removeLinkNodeListeners = (curLink: Link) => {
const ino = curLink.getNode().ino;
const removers = this._listenerRemovers.get(ino);
if (removers) {
removers.forEach(r => r());
this._listenerRemovers.delete(ino);
}
Object.values(curLink.children).forEach(childLink => {
if (childLink) {
removeLinkNodeListeners(childLink);
}
});
};
removeLinkNodeListeners(l);

this.emit('change', 'rename', relative(this._filename, l.getPath()));
};

// children nodes changed
Object.values(link.children).forEach(childLink => {
if (childLink) {
watchLinkNodeChanged(childLink);
}
});
// link children add/remove
link.on('child:add', onLinkChildAdd);
link.on('child:delete', onLinkChildDelete);

const removers = this._listenerRemovers.get(node.ino) ?? [];
removers.push(() => {
link.removeListener('child:add', onLinkChildAdd);
link.removeListener('child:delete', onLinkChildDelete);
});

if (recursive) {
Object.values(link.children).forEach(childLink => {
if (childLink) {
watchLinkChildrenChanged(childLink);
}
});
}
};
watchLinkNodeChanged(this._link);
watchLinkChildrenChanged(this._link);

const parent = this._link.parent;
if (parent) {
Expand All @@ -2769,7 +2837,10 @@ export class FSWatcher extends EventEmitter {
close() {
clearTimeout(this._timer);

this._link.getNode().removeListener('change', this._onNodeChange);
this._listenerRemovers.forEach(removers => {
removers.forEach(r => r());
});
this._listenerRemovers.clear();

const parent = this._link.parent;
if (parent) {
Expand Down

0 comments on commit 5e7c1dc

Please sign in to comment.