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