Skip to content

Commit

Permalink
refactor(core): refactor watch use FileSystemWatcher
Browse files Browse the repository at this point in the history
  • Loading branch information
why520crazy committed Sep 14, 2021
1 parent e68bed1 commit c6d7dd1
Show file tree
Hide file tree
Showing 9 changed files with 283 additions and 78 deletions.
2 changes: 1 addition & 1 deletion .wpmrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ module.exports = {
commitAll: true,
hooks: {
prepublish: 'yarn run build',
postbump: 'yarn sync-template-version --version {{version}} && lerna version {{version}} && git add .'
prebump: 'yarn sync-template-version --version {{version}} && lerna version {{version}} && git add .'
}
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"@angular/platform-browser-dynamic": "~10.2.4",
"@angular/router": "~10.2.4",
"@angularclass/hmr": "^2.1.3",
"@types/watchpack": "^1.1.7",
"chokidar": "^3.3.1",
"cosmiconfig": "^6.0.0",
"fancy-log": "^1.3.3",
Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/builders/components-builder.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { DocgeniContext } from '../docgeni.interface';
import { DocgeniHost } from '../docgeni-host';
import { toolkit } from '@docgeni/toolkit';
import { normalize, relative, resolve } from '../fs';
import { HostWatchEventType } from '../fs/node-host';
import { normalize, relative, resolve, HostWatchEventType } from '../fs';

export interface ComponentDef {
name: string;
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/fs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './docgeni-scoped';
export * from './node-host';
export * from './path';
export * from './host';
export * from './watcher';
68 changes: 30 additions & 38 deletions packages/core/src/fs/node-host.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,61 +5,53 @@ import fs from 'fs';
import temp from 'temp';
import { Observable, Subscription } from 'rxjs';
import { linuxAndDarwinIt, linuxOnlyIt } from '../testing';
import { toolkit } from '@docgeni/toolkit';

describe('DocgeniNodeJsAsyncHost', () => {
let root: string;
let host: virtualFs.Host<fs.Stats>;
let host: DocgeniNodeJsAsyncHost;

beforeEach(() => {
root = temp.mkdirSync('core-node-spec-');
host = new virtualFs.ScopedHost(new DocgeniNodeJsAsyncHost(), normalize(root));
host = new DocgeniNodeJsAsyncHost();
});

afterEach(done =>
host
.delete(normalize('/'))
.toPromise()
.then(done, done.fail)
);
afterEach(async () => {
await host.delete(normalize(root)).toPromise();
});

it('should get correct result for exists', async () => {
let isExists = await host.exists(normalize('not-found')).toPromise();
let isExists = await host.exists(normalize(root + '/not-found')).toPromise();
expect(isExists).toBe(false);
await host.write(normalize('not-found'), virtualFs.stringToFileBuffer('content')).toPromise();
isExists = await host.exists(normalize('not-found')).toPromise();
await host.write(normalize(root + '/not-found'), virtualFs.stringToFileBuffer('content')).toPromise();
isExists = await host.exists(normalize(root + '/not-found')).toPromise();
expect(isExists).toBe(true);
});

linuxAndDarwinIt('watch', done => {
let obs: Observable<virtualFs.HostWatchEvent>;
let subscription: Subscription;
it('watch', async () => {
const content = virtualFs.stringToFileBuffer('hello world');
const content2 = virtualFs.stringToFileBuffer('hello world 2');
const allEvents: virtualFs.HostWatchEvent[] = [];

Promise.resolve()
.then(() => fs.mkdirSync(root + '/sub1'))
.then(() => fs.writeFileSync(root + '/sub1/file1', 'hello world'))
.then(() => {
obs = host.watch(normalize('/sub1'), { recursive: true })!;
expect(obs).not.toBeNull();
subscription = obs.subscribe(event => {
allEvents.push(event);
});
})
// eslint-disable-next-line no-restricted-globals
.then(() => new Promise(resolve => setTimeout(resolve, 10)))
// Discard the events registered so far.
.then(() => allEvents.splice(0))
.then(() => host.write(normalize('/sub1/sub2/file3'), content).toPromise())
.then(() => host.write(normalize('/sub1/file2'), content2).toPromise())
.then(() => host.delete(normalize('/sub1/file1')).toPromise())
// eslint-disable-next-line no-restricted-globals
.then(() => new Promise(resolve => setTimeout(resolve, 3000)))
.then(() => {
expect(allEvents.length).toBe(3);
subscription.unsubscribe();
})
.then(done, done.fail);
const file1Path = root + '/sub1/file1.txt';
const file2Path = root + '/sub1/file2.txt';
const file3Path = root + '/sub1/sub2/file3.txt';

fs.mkdirSync(root + '/sub1');
fs.writeFileSync(file1Path, 'hello world');
const obs = host.watch(root + '/sub1', { recursive: true, ignoreInitial: true })!;
expect(obs).not.toBeNull();
const subscription = obs.subscribe(event => {
allEvents.push(event);
});
await toolkit.utils.wait(10);
await host.write(normalize(file3Path), content).toPromise();
await toolkit.utils.wait(10);
await host.write(normalize(file2Path), content2).toPromise();
await toolkit.utils.wait(10);
await host.delete(normalize(file1Path)).toPromise();
await toolkit.utils.wait(3000);
expect(allEvents.length).toBe(3);
subscription.unsubscribe();
});
});
41 changes: 5 additions & 36 deletions packages/core/src/fs/node-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { toolkit } from '@docgeni/toolkit';
import { FSWatcher, WatchOptions } from 'chokidar';
import { constants, PathLike, promises as fsPromises } from 'fs';
import { Observable, from } from 'rxjs';
import { publish, refCount } from 'rxjs/operators';
import { publish, refCount, tap } from 'rxjs/operators';
import { FileSystemWatcher } from './watcher';

async function exists(path: PathLike): Promise<boolean> {
try {
Expand All @@ -15,50 +16,18 @@ async function exists(path: PathLike): Promise<boolean> {
}
}

export enum HostWatchEventType {
Changed = 0,
Created = 1,
Deleted = 2,
Renamed = 3
}

export type DocgeniHostWatchOptions = WatchOptions & {
recursive?: boolean;
};

export class DocgeniNodeJsAsyncHost extends NodeJsAsyncHost {
exists(path: Path): Observable<boolean> {
return from(exists(getSystemPath(path)));
}

watch(path: string, options?: DocgeniHostWatchOptions): Observable<virtualFs.HostWatchEvent> {
options = { persistent: true, recursive: true, ...options };
return new Observable<virtualFs.HostWatchEvent>(obs => {
const watcher = new FSWatcher(options);
watcher.add(getSystemPath(normalize(path)));
watcher
.on('change', path => {
obs.next({
path: normalize(path),
time: new Date(),
type: (HostWatchEventType.Changed as unknown) as virtualFs.HostWatchEventType
});
})
.on('add', path => {
obs.next({
path: normalize(path),
time: new Date(),
type: (HostWatchEventType.Created as unknown) as virtualFs.HostWatchEventType
});
})
.on('unlink', path => {
obs.next({
path: normalize(path),
time: new Date(),
type: (HostWatchEventType.Deleted as unknown) as virtualFs.HostWatchEventType
});
});

return () => watcher.close();
}).pipe(publish(), refCount());
const watcher = new FileSystemWatcher(options);
return watcher.watch(path);
}
}
104 changes: 104 additions & 0 deletions packages/core/src/fs/watcher.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { virtualFs } from '@angular-devkit/core';
import temp from 'temp';
import fs from 'fs';
import { DocgeniNodeJsAsyncHost } from './node-host';
import { normalize, resolve } from './path';
import { Observable } from 'rxjs';
import { FileSystemWatcher, HostWatchEventType } from './watcher';
import { toolkit } from '@docgeni/toolkit';
import chokidar from 'chokidar';

describe('#fs-watcher', () => {
let root: string;
let host: virtualFs.Host<fs.Stats>;

beforeEach(() => {
root = temp.mkdirSync('core-node-spec-');
host = new DocgeniNodeJsAsyncHost();
});

afterEach(async () => {
await host.delete(normalize(root)).toPromise();
});

it('watch files', async () => {
const fsWatcher = new FileSystemWatcher();
const content = virtualFs.stringToFileBuffer('new content');
const file1Path = root + '/sub1/file1.txt';
const file2Path = root + '/sub1/file2.txt';
const file3Path = root + '/sub1/file3.txt';
fs.mkdirSync(root + '/sub1');
fs.writeFileSync(file1Path, 'hello world');
fs.writeFileSync(file2Path, 'hello world');

const allEvents: virtualFs.HostWatchEvent[] = [];
fsWatcher.watch([file1Path, file2Path]).subscribe(change => {
allEvents.push(change);
});
await toolkit.utils.wait(10);
await host.write(normalize(file1Path), content).toPromise();
await toolkit.utils.wait(10);
await host.write(normalize(file3Path), content).toPromise();
await toolkit.utils.wait(10);
await host.delete(normalize(file2Path)).toPromise();
await toolkit.utils.wait(2000);
expect(allEvents.length).toEqual(2);
await fsWatcher.close();
});

it('watch dir', async () => {
const fsWatcher = new FileSystemWatcher();
const content = virtualFs.stringToFileBuffer('hello world');
const file1Path = root + '/sub1/file1.txt';
const file2Path = root + '/sub1/file2.txt';
const file3Path = root + '/sub1/sub2/file3.txt';
fs.mkdirSync(root + '/sub1');
fs.writeFileSync(file1Path, 'hello world');

const allEvents: virtualFs.HostWatchEvent[] = [];
fsWatcher.watch(root + '/sub1').subscribe(change => {
allEvents.push(change);
});
await toolkit.utils.wait(10);
await host.write(normalize(file2Path), content).toPromise();
await toolkit.utils.wait(10);
await host.write(normalize(file3Path), content).toPromise();
await toolkit.utils.wait(10);
await host.delete(normalize(file1Path)).toPromise();
await toolkit.utils.wait(2000);
expect(allEvents.length).toEqual(3);
await fsWatcher.close();
});

it('watch aggregated', async () => {
const fsWatcher = new FileSystemWatcher();
const content = virtualFs.stringToFileBuffer('new content');
const file1Path = root + '/sub1/file1.txt';
const file2Path = root + '/sub1/file2.txt';
const file3Path = root + '/sub1/file3.txt';
fs.mkdirSync(root + '/sub1');
fs.writeFileSync(file1Path, 'hello world');
fs.writeFileSync(file2Path, 'hello world');

let allEvents: virtualFs.HostWatchEvent[] = [];
fsWatcher.watch(root + '/sub1');
fsWatcher.aggregated(3000).subscribe(value => {
allEvents = value;
});
await toolkit.utils.wait(10);
await host.write(normalize(file1Path), content).toPromise();
await toolkit.utils.wait(10);
await host.write(normalize(file3Path), content).toPromise();
await toolkit.utils.wait(10);
await host.delete(normalize(file2Path)).toPromise();
await toolkit.utils.wait(4000);
expect(allEvents.length).toEqual(3);
expect(allEvents[0].path).toEqual(file1Path);
expect(allEvents[0].type).toEqual(HostWatchEventType.Changed);
expect(allEvents[1].path).toEqual(file3Path);
expect(allEvents[1].type).toEqual(HostWatchEventType.Created);
expect(allEvents[2].path).toEqual(file2Path);
expect(allEvents[2].type).toEqual(HostWatchEventType.Deleted);
await fsWatcher.close();
});
});
101 changes: 101 additions & 0 deletions packages/core/src/fs/watcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { Observable, Subject } from 'rxjs';
import { FSWatcher, WatchOptions } from 'chokidar';
import { getSystemPath, virtualFs } from '@angular-devkit/core';
import { normalize } from './path';
import { toolkit } from '@docgeni/toolkit';

export enum HostWatchEventType {
Changed = 0,
Created = 1,
Deleted = 2,
Renamed = 3
}

export class FileSystemWatcher {
private watcher: FSWatcher;
private events$ = new Subject<virtualFs.HostWatchEvent>();
private aggregatedEvents$ = new Subject<virtualFs.HostWatchEvent[]>();
private aggregateTimeout: NodeJS.Timeout;
private aggregatedEvents: virtualFs.HostWatchEvent[] = [];

constructor(options: WatchOptions = { persistent: true, ignoreInitial: true }) {
this.watcher = new FSWatcher(options);
this.initialize();
}

private emitEvent(path: string, type: HostWatchEventType) {
this.events$.next({
path: normalize(path),
time: new Date(),
type: (type as unknown) as virtualFs.HostWatchEventType
});
}

private initialize() {
this.watcher
.on('change', path => {
this.emitEvent(path, HostWatchEventType.Changed);
})
.on('add', path => {
this.emitEvent(path, HostWatchEventType.Created);
})
.on('unlink', path => {
this.emitEvent(path, HostWatchEventType.Deleted);
});
}

watch(paths: string | string[]): Observable<virtualFs.HostWatchEvent> {
// this.watcher.add(paths);
this.watcher.add(toolkit.utils.coerceArray(paths).map(path => getSystemPath(normalize(path))));
return this.events$.asObservable();
}

aggregated(aggregateInterval: number = 2000): Observable<virtualFs.HostWatchEvent[]> {
// return new Observable(subscribe => {
// const aggregatedEvents: virtualFs.HostWatchEvent[] = [];
// let aggregateTimeout: NodeJS.Timeout;
// const subscription = this.events$.subscribe(event => {
// if (aggregateTimeout) {
// // eslint-disable-next-line no-restricted-globals
// clearTimeout(aggregateTimeout);
// }
// aggregatedEvents.push(event);

// // eslint-disable-next-line no-restricted-globals
// aggregateTimeout = setTimeout(() => {
// subscribe.next(aggregatedEvents);
// }, aggregateInterval);
// });
// return () => subscription.unsubscribe();
// });
this.events$.subscribe(event => {
if (this.aggregateTimeout) {
// eslint-disable-next-line no-restricted-globals
clearTimeout(this.aggregateTimeout);
}
this.aggregatedEvents.push(event);

// eslint-disable-next-line no-restricted-globals
this.aggregateTimeout = setTimeout(() => {
this.aggregatedEvents$.next(this.aggregatedEvents);
this.aggregatedEvents = [];
this.aggregateTimeout = null;
}, 2000);
});
return this.aggregatedEvents$;
}

async close() {
if (this.aggregateTimeout) {
// eslint-disable-next-line no-restricted-globals
clearTimeout(this.aggregateTimeout);
this.aggregatedEvents$.next(this.aggregatedEvents);
this.aggregatedEvents = [];
this.aggregateTimeout = null;
}
this.events$.complete();
this.aggregatedEvents$.complete();
this.watcher.removeAllListeners();
await this.watcher.close();
}
}

0 comments on commit c6d7dd1

Please sign in to comment.