Discord.js Clientのlogin/destroyとpromise/イベントの関係

Discord.jsの`Client`クラスは、Discordサーバとの接続やBotの情報を参照するためのインターフェースを提供してくれる。サーバとの接続/切断は2つのメソッド`login`/`destroy`で行い、接続状態の変化は4つのイベント`ready`, `reconnecting`, `resume`, `disconnect`で知ることができるようになっている。

このClientを複数の機能から共有して、必要な時だけ接続しようとしたところ、期待した動きと違うことに気付いた。いくつか思いつく限り調べてみたので結果をまとめる。

#期待したのと違う動き
`login`後に`ready`イベントが発生しないうちに`destroy`すると接続が切れなかった。てっきり中断してくれるかと思ったが、そういうわけではなかった。以下の例を実行すると、`ready`イベントしか発生せず、接続は保たれたままになってしまう。

```typescript:接続が切断できなくなる例
let client = new Client();
client.on( 'ready',        () => { console.log( 'ready' ) } );
client.on( 'disconnect',   () => { console.log( 'disconnect' ) } );
client.on( 'reconnecting', () => { console.log( 'reconnecting' ) } );
client.on( 'reconnecting', () => { console.log( 'resume' ) } );

client.login( TOKEN );
client.destroy();
```

他にも、`ready`後に`login`するとpromiseが完了しない(=awaitできない)ことも分かった。

# login/destroyとpromise/イベントの関係
Clientの動きを確かめた結果(実験は以降「login/destroyとpromise/イベントの関係の実験」参照)をまとめると以下の表のようになる。

* login
 * 戻り値のpromiseは`ready`への変化と関係ありそうだが、資料に明記なかった
 * 接続済(ready/resume)でコールすると、promiseは完了しない(例外も起きない)★
 * 切断中(destroy後)にコールすると、切断したのち接続する(切断とは無関係?)
* destroy
 * promiseは実際の切断と無関係に完了する
 * 接続中にdestroyしても、接続を中断しない★
* ready/disconnectイベント
 * login/destroyと関係なく、クライアントの状態が変わった時に発生する。(これは普通)

|初期状態|試したこと|イベント|promise|
|:-----------------|:--------------------------|:----------|:------|
|未接続|login後readyを待たたずにlogin|readyが1回だけ発生|全login/destroyのpromiseが完了|
|未接続|login後readyを待たずにdestroy|readyが1回だけ発生(disconnectなし)|全login/destroyのpromiseが完了|
|未接続|loginせずにdestroy|発生せず|全login/destroyのpromiseが完了|
|接続済|destroyせずにlogin|発生せず|ready後のloginのpromiseが完了せず★|
|接続済|destroy後disconnectを待たずにdestroy|disconnectが1回だけ発生|全destroyのpromiseが完了|
|接続済|destroy後disconnectを待たずにlogin|disconnect、readyを繰り返す★|destroy後のloginのpromiseが完了せず(1回目だけ)★|

## この動作が意味すること
`login`と`destroy`の振る舞いが外から見えない内部状態(実際にはStatusがあるらしいが)で変わるということ。例えば、接続中に`login`しても、promiseは完了しないし`ready`イベントもないので、いつクライアントが使えるようになるかを知ることができない。

素のままだと複数のモジュールから1つの`Client`を共有するのは難しそうな気がする。`Client`を拡張し、接続・切断の完了を戻り値のpromiseの完了で知ることができるようにすれば良いかもしれない。

# login/destroyとpromise/イベントの関係の実験
## 未接続状態からの操作
### loginのPromiseとreadyイベントどちらが早い?
`login`の戻り値であるPromiseの発生と、`ready`イベントはどちらが先になるか、discord.jsの資料に記載がない。`login`の戻り値は「Token of the account used.」と書かれているだけで、`ready`も「Emitted when the client becomes ready to start working.」と書かれているだけなので、両者の関係は分からない。「使えるようになる」という意味で見ると、`ready`後に操作した方が良いとは思う。

実験の結果、`ready`イベントの方が先にあることが分かった。

```typescript
    let client = new Client();
    client.on( 'ready',        () => { console.log( 'ready' ) } );
    client.on( 'disconnect',   () => { console.log( 'disconnect' ) } );

    await client.login( TOKEN );
    console.log( 'login promsie' );
    client.destroy();
// 結果:
// ready
// login promise
// disconnect
```

### login後readyを待たずにlogin
`ready`イベントの方が早いのは変わらず、`ready`と`disconnect`イベントは1つずつしか起きないことが分かった。`login`のpromiseはそれぞれ完了していた。

```typescript
    let client = new Client();
    client.on( 'ready',        () => { console.log( 'ready' ) } );
    client.on( 'disconnect',   () => { console.log( 'disconnect' ) } );

    console.log( 'login 1回目' )
    client.login( TOKEN ).then( () => console.log( 'login promise(1)' ) );
    console.log( 'login 2回目' )
    client.login( TOKEN ).then( () => console.log( 'login promise(2)' ) );
    console.log( 'login 3回目(待機)' )
    await client.login( TOKEN ).then( () => console.log( 'login promise(3)' ) );

    client.destroy();

// 結果:
//    login 1回目
//    login 2回目
//    login 3回目(待機)
//    ready
//    login promise(1)
//    login promise(2)
//    login promise(3)
//    disconnect
```

### login後readyを待たずにdestroy
`login`をコール後、接続している途中で`destroy`をコールすると、`ready`イベントの前に`destroy`のpromiseが完了することが分かった。`disconnect`イベントは発生せず、プログラムが終了した時点で接続が保持されていた。

接続している途中 = 接続していないから、`destroy`がすぐに成功したのかもしれない。後の操作が勝つのではなかった。

```typescript
    let client = new Client();
    client.on( 'ready',        () => { console.log( 'ready' ) } );
    client.on( 'disconnect',   () => { console.log( 'disconnect' ) } );

    console.log( 'login' );
    client.login( TOKEN ).then( () => console.log( 'login promise' ) );
    console.log( 'destroy' );
    client.destroy().then( () => console.log( 'destroy promise' ) );

// 結果:
//    login
//    destroy
//    destroy promise
//    ready
//    login promise
```

### loginせずにdestroy
接続していない状態で`destroy`をコールすると、promiseがすぐに完了し、イベントは発生しないことが分かった。状態が変化しないからだと思う。

```typescript
    let client = new Client();
    client.on( 'ready',        () => { console.log( 'ready' ) } );
    client.on( 'disconnect',   () => { console.log( 'disconnect' ) } );

    console.log( 'destroy 1回目' );
    client.destroy().then( () => console.log( 'destroy promise(1)' ) );
    console.log( 'destroy 2回目' );
    client.destroy().then( () => console.log( 'destroy promise(2)' ) );

// 結果:
//    destroy 1回目
//    destroy 2回目
//    destroy promise(1)
//    destroy promise(2)
```
## 接続済(ready後)からの操作
### destroyせずにlogin
`ready`後の`login`は完了しないことが分かった。Promiseがsolveせず、`await`で待機しようとすると次に進めなくなり、`destroy`に到達しなくなってしまった。

```typescript
    let client = new Client();
    client.on( 'ready', async () => {
        console.log( 'ready' ); 
        
        console.log( 'login ready後 1回目' );
        client.login( TOKEN ).then( () => console.log( 'login promise(1)' ) );

        console.log( 'login ready後 2回目(待機)' );
        await client.login( TOKEN ).then( () => console.log( 'login promise(2)' ) );
        
        console.log( 'destroy' );
        client.destroy().then( () => console.log( 'destroy promise' ) );
    } );
    
    client.on( 'disconnect',   () => { console.log( 'disconnect' ) } );

    console.log( 'login' ); 
    client.login( TOKEN ).then( () => console.log( 'login promise' ) );

// 結果:
//    login
//    ready
//    login ready後 1回目
//    login ready後 2回目(待機)
//    login promise
```

### destroy後disconnectを待たずにdestroy
`ready`後、`destroy`を続けてコールすると、`ready`のコールバック内でpromiseまで完了することが分かった。コールバックが抜けた後、`login`のpromiseが完了し、切断後`disconnect`イベントが発生した。

`login`と違い、`destroy`は素直な動きをしている。

```typescript
    let client = new Client();
    client.on( 'ready', async () => {
        console.log( 'ready' ); 
        
        console.log( 'destroy 1回目' );
        client.destroy().then( () => console.log( 'destroy promise(1)' ) );

        console.log( 'destroy 2回目' );
        client.destroy().then( () => console.log( 'destroy promise(2)' ) );
    } );
    client.on( 'disconnect',   () => { console.log( 'disconnect' ) } );

    console.log( 'login' ); 
    client.login( TOKEN ).then( () => console.log( 'login promise' ) );
    
// 結果:
//    login
//    ready
//    destroy 1回目
//    destroy 2回目
//    destroy promise(1)
//    destroy promise(2)
//    login promise
//    disconnect
```

### destroy後disconnectを待たずにlogin
`ready`後、`destroy`をコール氏、すぐに`login`をコールすると、そのpromiseは完了しないものの、`disconnect`後再度`ready`になることが分かった。以降、切断と接続を繰り返してしまう。しかも、2回目以降は`ready後のlogin`のpromiseも成功するようになって不可解。

先ほどの`login`中の`destroy`と合わせて考えると、`login`の方が強いのかもしれない。

```typescript
    let client = new Client();
    client.on( 'ready', async () => {
        console.log( 'ready' ); 
        
        console.log( 'destroy' );
        client.destroy().then( () => console.log( 'destroy promise(1)' ) );

        console.log( 'ready後 login' );
        client.destroy().then( () => console.log( 'after ready login promise' ) );
    } );
    client.on( 'disconnect',   () => { console.log( 'disconnect' ) } );

    console.log( 'login' ); 
    client.login( TOKEN ).then( () => console.log( 'login promise' ) );

// 結果:
//    login
//    ready
//      destroy
//      ready後 login
//      destroy promise(1)
//    login promise
//    disconnect
// 　 ここから2回目の接続
//    ready
//      destroy
//      ready後 login
//      destroy promise(1)
//      after ready login promise
//    disconnect
// 　 ここから3回目の接続
//    ready
//      destroy
//      ready後 login
//      destroy promise(1)
//      after ready login promise
//    disconnect
```