Discord.jsのClientクラスは、Discordサーバとの接続やBotの情報を参照するためのインターフェースを提供してくれる。サーバとの接続/切断は2つのメソッドlogin/destroyで行い、接続状態の変化は4つのイベントready, reconnecting, resume, disconnectで知ることができるようになっている。
このClientを複数の機能から共有して、必要な時だけ接続しようとしたところ、期待した動きと違うことに気付いた。いくつか思いつく限り調べてみたので結果をまとめる。
期待したのと違う動き
login後にreadyイベントが発生しないうちにdestroyすると接続が切れなかった。てっきり中断してくれるかと思ったが、そういうわけではなかった。以下の例を実行すると、readyイベントしか発生せず、接続は保たれたままになってしまう。
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イベントの方が先にあることが分かった。
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はそれぞれ完了していた。
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がすぐに成功したのかもしれない。後の操作が勝つのではなかった。
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がすぐに完了し、イベントは発生しないことが分かった。状態が変化しないからだと思う。
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に到達しなくなってしまった。
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は素直な動きをしている。
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の方が強いのかもしれない。
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