Skip to content
This repository has been archived by the owner on Jul 7, 2023. It is now read-only.

Make watchman-processor librar-ish #133

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion bin/watchman-processor
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
#!/usr/bin/env node

process.title = 'watchman-processor';
var watchman = require('../index');

var index = require('../index');
var watchman = index.processor;
var terminal = index.terminal;
var WatchmanProcessorEvent = index.WatchmanProcessorEvent;

if (watchman && typeof watchman.start === 'function') {
watchman.emitter.on(WatchmanProcessorEvent.Error, function (params) {
terminal.error(params.err);
});
watchman.emitter.on(WatchmanProcessorEvent.Debug, function (params) {
terminal.debug(params.msg);
});
watchman.emitter.on(WatchmanProcessorEvent.SetState, function (params) {
terminal.setState(params.configEntry, params.state, params.statusMessage);
});
watchman.emitter.on(WatchmanProcessorEvent.Render, terminal.render.bind(terminal));
watchman.start()
}
process.on('SIGINT', function() {
Expand Down
3 changes: 2 additions & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module.exports = {
external: [
'chai',
'child_process',
'events',
'fb-watchman',
'fs',
'inversify',
Expand All @@ -26,4 +27,4 @@ module.exports = {
typescript: require('typescript')
}),
]
};
};
50 changes: 27 additions & 23 deletions src/WatchmanProcessor.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { EventEmitter } from 'events';
import { Client, SubscriptionResponse, SubscriptionResponseFile } from 'fb-watchman';
import { inject, injectable } from 'inversify';
import { resolve as resolvePath } from 'path';
import { Config, SubConfig, Sync, Terminal, WatchmanExpression, WatchmanProcessor } from '../interfaces';
import { Config, SubConfig, Sync, WatchmanExpression, WatchmanProcessor } from '../interfaces';
import { Bindings } from './ioc.bindings';
import { WatchmanProcessorEvent } from './WatchmanProcessorEvent';

@injectable()
export class WatchmanProcessorImpl implements WatchmanProcessor {
Expand All @@ -11,16 +13,16 @@ export class WatchmanProcessorImpl implements WatchmanProcessor {
private config: Config,
@inject(Bindings.WatchmanClient)
private client: Client,
@inject(Bindings.Terminal)
private terminal: Terminal,
@inject(Bindings.Emitter)
private emitter: EventEmitter,
@inject(Bindings.Sync)
private sync: Sync,
) { }

public start(): void {
const { client, terminal } = this;
const { client, emitter } = this;

terminal.debug('watchman: initialize');
emitter.emit(WatchmanProcessorEvent.Debug, {msg: 'watchman: initialize'});
const onCapabilityCheck = this.onCapabilityCheck.bind(this);
client.capabilityCheck({}, onCapabilityCheck);
}
Expand All @@ -46,12 +48,12 @@ export class WatchmanProcessorImpl implements WatchmanProcessor {
}

private onCapabilityCheck(error?: string | Error): void {
const terminal = this.terminal;
const emitter = this.emitter;
if (error) {
terminal.error(error);
emitter.emit(WatchmanProcessorEvent.Error, {err: error });
return;
}
terminal.render();
emitter.emit(WatchmanProcessorEvent.Render);

const client = this.client;
const onSubscription = this.onSubscription.bind(this);
Expand All @@ -68,8 +70,8 @@ export class WatchmanProcessorImpl implements WatchmanProcessor {

promises.push(this.subscribe(resolvePath(sub.source), name, expression));
}
const render = terminal.render.bind(terminal);
const errHandler = terminal.error.bind(terminal);
const render = emitter.emit.bind(emitter, WatchmanProcessorEvent.Render);
const errHandler = emitter.emit.bind(emitter, WatchmanProcessorEvent.Error);
Promise.all(promises).then(render).catch(errHandler);

// subscription is fired regardless of which subscriber fired it
Expand All @@ -82,56 +84,58 @@ export class WatchmanProcessorImpl implements WatchmanProcessor {
const files = resp.files;

const subConfig = config.subscriptions[subscription];
this.syncFiles(subConfig, files);
this.syncFiles(subConfig, files, subscription);
}

private syncFiles(subConfig: SubConfig, files: SubscriptionResponseFile[]): void {
const {sync, terminal } = this;
terminal.setState(subConfig, 'running');
private syncFiles(subConfig: SubConfig, files: SubscriptionResponseFile[], subscription: string): void {
const {sync, emitter } = this;
emitter.emit(WatchmanProcessorEvent.SetState, {subscription, configEntry: subConfig, state: 'running' });

const fileNames = (files || []).map(file => file.name);
sync.syncFiles(subConfig, fileNames)
.then(() => {
terminal.setState(subConfig, 'good');
emitter.emit(WatchmanProcessorEvent.SetState, {subscription, configEntry: subConfig, state: 'good' });
})
.catch(err => {
terminal.setState(subConfig, 'error', err);
emitter.emit(
WatchmanProcessorEvent.SetState,
{subscription, configEntry: subConfig, state: WatchmanProcessorEvent.Error, statusMessage: err});
});
}

private subscribe(folder: string, name: string, expression: WatchmanExpression): Promise<void> {
const terminal = this.terminal;
const emitter = this.emitter;
const client = this.client;
const sub = {
expression,
fields: ['name', 'exists'],
relative_root: '',
};

terminal.debug(`subscribe: ${name}`);
emitter.emit(WatchmanProcessorEvent.Debug, {msg: `subscribe: ${name}` });
return new Promise<void>((resolve, reject) => {
client.command(['subscribe', folder, name, sub],
(error: string) => {
error ? reject('failed to start: ' + error) : resolve();
error ? reject({err: 'failed to start: ' + error, subscription: name}) : resolve();
});
});
}

private unsubscribe(folder: string, name: string): Promise<string | void> {
const terminal = this.terminal;
const emitter = this.emitter;
const client = this.client;

terminal.debug(`unsubscribe: ${name}`);
emitter.emit(WatchmanProcessorEvent.Debug, {msg: `unsubscribe: ${name}` });
return new Promise<string | void>(resolve => {
client.command(['unsubscribe', folder, name], resolve);
});
}

private shutdown(): Promise<string | void> {
const terminal = this.terminal;
const emitter = this.emitter;
const client = this.client;

terminal.debug(`watchman: shutdown`);
emitter.emit(WatchmanProcessorEvent.Debug, {msg: `watchman: shutdown` });
return new Promise<string | void>(resolve => {
client.command(['shutdown-server'], resolve);
});
Expand Down
8 changes: 8 additions & 0 deletions src/WatchmanProcessorEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
enum WatchmanProcessorEvent {
Debug = 'debug',
Error = 'error',
Render = 'render',
SetState = 'setState',
}

export {WatchmanProcessorEvent};
11 changes: 9 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import 'reflect-metadata';

import { Cli, ConfigManager, WatchmanProcessor } from '../interfaces';
import { Cli, ConfigManager, Terminal, WatchmanProcessor } from '../interfaces';
import { Bindings } from './ioc.bindings';
import { container } from './ioc.config';
import { WatchmanProcessorEvent } from './WatchmanProcessorEvent';

const cli = container.get<Cli>(Bindings.Cli);
const configManager = container.get<ConfigManager>(Bindings.ConfigManager);
const watchmanProcessor = container.get<WatchmanProcessor>(Bindings.WatchmanProcessor);
const terminal = container.get<Terminal>(Bindings.Terminal);

const args = cli.getArguments();
let processor = watchmanProcessor;
Expand All @@ -28,4 +30,9 @@ if (args.init) {
}
}

export default processor;
export default {
WatchmanProcessorEvent,
config: configManager.getConfig(),
processor,
terminal,
};
1 change: 1 addition & 0 deletions src/ioc.bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export const Bindings = {
Cli: Symbol('Cli'),
Config: Symbol('Config'),
ConfigManager: Symbol('ConfigManager'),
Emitter: Symbol('Emitter'),
Process: Symbol('Process'),
Require: Symbol('require'),
Spawn: Symbol('spawn'),
Expand Down
2 changes: 2 additions & 0 deletions src/ioc.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { spawn } from 'child_process';
import { EventEmitter } from 'events';
import { Client } from 'fb-watchman';
import { Container } from 'inversify';
import * as interfaces from '../interfaces';
Expand All @@ -16,6 +17,7 @@ container.bind<NodeJS.Process>(Bindings.Process).toConstantValue(process);
container.bind<interfaces.Spawn>(Bindings.Spawn).toConstantValue(spawn);
container.bind<NodeRequire>(Bindings.Require).toConstantValue(require);
container.bind<Client>(Bindings.WatchmanClient).toConstantValue(new Client());
container.bind<EventEmitter>(Bindings.Emitter).toConstantValue(new EventEmitter());

// setup the main classes
container.bind<interfaces.Cli>(Bindings.Cli).to(CliImpl);
Expand Down
43 changes: 28 additions & 15 deletions test/WatchmanProcessor-test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import * as chai from 'chai';
import { EventEmitter } from 'events';
import { Client } from 'fb-watchman';
import 'reflect-metadata';
import * as sinon from 'sinon';
import 'ts-helpers';
import { Config } from '../interfaces';
import { SyncImpl as Sync } from '../src/Sync';
import { TerminalImpl as Terminal } from '../src/Terminal';
import { WatchmanProcessorImpl as Watchman } from '../src/WatchmanProcessor';
import { WatchmanProcessorEvent } from '../src/WatchmanProcessorEvent';

const mockTerminal = sinon.mock(Terminal);
const terminal: Terminal = mockTerminal as any;
const mockEventEmitter = sinon.mock(EventEmitter);
const emitter = mockEventEmitter as any;
const mockSync = {
end: sinon.stub(),
syncFiles: sinon.stub(),
Expand Down Expand Up @@ -38,10 +39,7 @@ describe('Watchman', () => {

beforeEach(() => {
// mock all the defaults before
terminal.render = sinon.stub();
terminal.error = sinon.stub();
terminal.debug = sinon.stub();
terminal.setState = sinon.stub();
emitter.emit = sinon.spy();

mockSync.syncFiles = sinon.stub().returns(new Promise(resolve => resolve()));
mockWatchmanClient.capabilityCheck = sinon.stub().callsArg(1);
Expand All @@ -50,57 +48,72 @@ describe('Watchman', () => {
});

it('should start watchman', () => {
const watchman = new Watchman(config, watchmanClient, terminal, sync);
const watchman = new Watchman(config, watchmanClient, emitter, sync);
watchman.start();

chai.assert.isObject(watchman, 'watchman is an object');
});

it('should log errors from watchman.capabilityCheck', () => {
mockWatchmanClient.capabilityCheck.callsArgWith(1, 'error');
mockWatchmanClient.capabilityCheck.callsArgWith(1, WatchmanProcessorEvent.Error);

const watchman = new Watchman(config, watchmanClient, terminal, sync);
const watchman = new Watchman(config, watchmanClient, emitter, sync);
watchman.start();

chai.assert.deepEqual(
emitter.emit.getCall(0).args,
[WatchmanProcessorEvent.Debug, { msg: 'watchman: initialize' }]);
chai.assert.deepEqual(emitter.emit.getCall(1).args, [WatchmanProcessorEvent.Error, { err: 'error' }]);
chai.assert.isObject(watchman, 'watchman is an object');
});

it('should log errors from watchman.command', () => {
mockWatchmanClient.command.callsArgWith(1, 'error');

const watchman = new Watchman(config, watchmanClient, terminal, sync);
const watchman = new Watchman(config, watchmanClient, emitter, sync);
watchman.start();

chai.assert.deepEqual(
emitter.emit.getCall(0).args,
[WatchmanProcessorEvent.Debug, { msg: 'watchman: initialize' }]);
chai.assert.deepEqual(emitter.emit.getCall(1).args, [WatchmanProcessorEvent.Render]);
chai.assert.deepEqual(emitter.emit.getCall(2).args, [WatchmanProcessorEvent.Debug, { msg: 'subscribe: example1' }]);
chai.assert.deepEqual(emitter.emit.getCall(3).args, [WatchmanProcessorEvent.SetState,
{ configEntry: config.subscriptions.example1, state: 'running', subscription: 'example1' }]);
chai.assert.isObject(watchman, 'watchman is an object');
});

it('should log errors from sync.syncFiles', () => {
mockSync.syncFiles.returns(new Promise(() => { throw new Error('error'); }));

const watchman = new Watchman(config, watchmanClient, terminal, sync);
const watchman = new Watchman(config, watchmanClient, emitter, sync);
watchman.start();

chai.assert.isObject(watchman, 'watchman is an object');
});

it('should attempt to sync files', () => {
mockWatchmanClient.on = sinon.stub().callsArgWith(1, {subscription: 'example1'});
const watchman = new Watchman(config, watchmanClient, terminal, sync);
const watchman = new Watchman(config, watchmanClient, emitter, sync);
watchman.start();

chai.assert.isObject(watchman, 'watchman is an object');
});

it('should end', () => {
mockWatchmanClient.end = sinon.stub();
const watchman = new Watchman(config, watchmanClient, terminal, sync);
const watchman = new Watchman(config, watchmanClient, emitter, sync);
watchman.end();

chai.assert.deepEqual(
emitter.emit.getCall(0).args,
[WatchmanProcessorEvent.Debug, { msg: 'unsubscribe: example1' }]);
});

it('should end and shutdown', () => {
mockWatchmanClient.end = sinon.stub();
const newConfig = { controlWatchman: true, subscriptions: {} } as any;
const watchman = new Watchman(newConfig, watchmanClient, terminal, sync);
const watchman = new Watchman(newConfig, watchmanClient, emitter, sync);
watchman.end();
});

Expand Down