chokidar を Typescriptと仲良くさせる(fromEvent, async/await)

Typescriptの型チェックや、非同期処理を助けるasync/await、ObservableのfromEvent、utilのpromisifyなどにより、最近のライブラリは使いやすくなってきたと思う。

それでも、Javascriptでおなじみのコールバックやイベントエミッタを利用したライブラリとは仲良くしていかないといけない。例えば`chokidar`は、インスタンス生成後`ready`イベントまで待ってからでないと、`add`や`change`イベントのリスナを追加できない厄介な性質がある。

> 正確には追加できるのだが、初期スキャンで全ファイルが「add」扱いにされてしまう。これは大抵の人にとって意図しない動作だと思う。

# readyを待ってFSWatcherを返す
初期スキャンの完了(`ready`イベント発生)してからのウォッチャ(`FSWatcher`)が欲しければ、以下のようにする。もともとの`watch`関数の戻り値を得た後、`ready`を待ってから`resolve`で戻り値の`FSWatcher`を返している。

```typescript
function createWatcher( paths: string | string[], options: WatchOptions = {} ): Promise<FSWatcher> {
    return new Promise( ( resolve, reject ) => {
        try {
            let watcher = watch( paths, options );
            watcher.once( 'ready', () => resolve( watcher ) );
        } catch( err ) {
            reject( err );
        }
    } );
}
```

これで、以下のように`ready`後の`FSWatcher`が得られるようになる。

```typescript
let watcher = await createWatcher( <パス> );
```

# Observableを返すようにする
イベントのままだと`Observable`と仲良くないので、`rxjs`の`fromEvent`を使って`Observable`化する。
`fromEvent`を使う前に、いつイベントリスナが登録されるかというところが気になったので検証してみた。

##検証: fromEventでリスナが増えるのはいつ?
`FSWatcher`のインスタンス`watcher`に対して以下の操作を行ない、その間のリスナ数を調べてみた。

1. `add`イベントを追加
2. fromEventで`add`イベントをObservable化(add$と呼ぶ)
3. add$を購読(sb1と呼ぶ)
4. add$を購読(sb2と呼ぶ)
5. sb1の購読を停止
6. sb2の購読を停止

```typescript
// async関数内
let watcher = await createWatcher( <パス> );
console.log( '[0] listener: ' + watcher.listeners('add').length );
watcher.on( 'add', (path) => { console.log( '[on]: ' + path ) } );
console.log( '[1] listener: ' + watcher.listeners('add').length );

const add$ = fromEvent( watcher, 'add' );
console.log( '[2] listener: ' + watcher.listeners('add').length );

add$.subscribe( ( path ) => { 
    console.log( '[subscribe1]: ' + path );
} );
console.log( '[3] listener: ' + watcher.listeners('add').length );

add$.subscribe( ( path ) => {
    console.log( '[subscribe2]: ' + path );
} );
console.log( '[4] listener: ' + watcher.listeners('add').length );

sb1.unsubscribe();
console.log( '[5] listener: ' + watcher.listeners('add').length );

sb2.unsubscribe();
console.log( '[6] listener: ' + watcher.listeners('add').length );
```

実行結果から、以下のように購読するごとにリスナが増えることが分かった。購読(subscribe)しない限りリスナが増えないというのは便利だ。

```
[0] listener: 0
[1] listener: 1
[2] listener: 1
[3] listener: 2
[4] listener: 3
[5] listener: 2
[6] listener: 1
```

##検証: fromEventしてshareしたらどうなるか
先ほどの検証で、shareにしたらどうなるかを調べた。

```typescript
let add$ = fromEvent( watcher, 'add' ).pipe( share() );
```

実行結果から、購読が増えてもイベントリスナは増えない = `share`されていることが分かった。

```
[0] listener: 0
[1] listener: 1
[2] listener: 1
[3] listener: 2
[4] listener: 2 ← ここで購読が増えたが、リスナは増えない
[5] listener: 2
[6] listener: 1
```

## 困ったこと: 余計な情報が付いてくる
イベントリスナの場合、実はパスだけでなくファイルの情報を含んだ配列が生成されていることが分かった。推測だが`[ path(paths), stat ]`になっていると思う。

`fromEvent`だけだといらない情報が両方ついてきてしまって嬉しくないので、除外する必要がある。とりあえず戻り値の配列の[0]だけ取り出すようにする。

## Observableを返す関数
これらを踏まえ、今どきのTypescriptによる型チェックを活用して使いやすくするなら以下のようになると思う。`enum`にすることで、Eclipseなどで自動補間してくれるようになる。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/374475/4a97ccef-e60b-fb68-4e14-6172eda688a9.png)

```typescript
import { FSWatcher } from 'chokidar';
import { fromEvent } from 'rxjs';

type EventType = 'add' 
               | 'change'
               | 'unlink'
               | 'addDir'
               | 'unlinkDir';

function toObservable( watcher: FSWatcher, event: EventType ): Observable<string> {
    return fromEvent( watcher, event ).pipe( map( info => info[0] ) );
}
```

# 備考
`FSWatcher`インスタンスは、生成後に`add`で監視対象を増やすことができる。しかし、`add`イベントリスナを追加済みの状態で監視対象を増やすと、初回スキャンで既存のファイルを追加したとみなしてしまうらしい。本当の追加と区別がつけられるなら良いが、Observableとは相性が良くなさそうだった。