[JavaScript] コールバック関数を非同期イテレータで置き換える
1. 経緯
先日読んだNostrの記事が面白く、仕様を読んでクライアントを実装して遊んでみようと思いました。
Nostrのクライアントとリレー(サーバー)間は、WebSocketで通信するのですが、JavaScriptの WebSocket
は受信処理を message イベント用のハンドラを設定する方式でした。
const socket = new WebSocket("wss://example.com");
socket.onmessage = e => {
// ...
};
これはこれで良いのですが、Promise
を使う非同期処理に書き換えたら、見通しが良くなったりしないだろうかという興味が湧いて (当初の目的から脇に逸れて) 実装を考えてみました。
先に書いておくと、この方式により見通しが良くなったり実装が良くなったかと言うと微妙に感じています。とはいえ、何か使い道はあるかもしれないので、供養も兼ねて記事にしておこうと思います。
2. 実装
Promise
は一度しか使えないので、今回のように後からどんどんやってくる場合には、その数だけ順次 Promise
を生成する必要があり、非同期イテレータ を満たすクラスを実装することにしました。[1]
{ value, done }
を出力する Promise
を返す next()
メソッドを実装し、[Symbol.asyncIterator]()
メソッドで、this
を返すようにすれば、最低限の実装はできます。
コールバック用のメソッドを生やしておき、イベントハンドラとして登録する事で、Promise
が解決されます。[2]
利用側または解決側が呼びだされ、必要になった時点で、新しいPromise
を生成して、キューに積み順番に利用していきます。
class Callback2AsyncGenerator {
constructor(){
this.resolve = [];
this.promise = [];
this.done = false;
}
#generate(){
this.promise.push(new Promise(
this.done ?
r => r({ done: true }) :
r => this.resolve.push(r)
));
}
[Symbol.asyncIterator](){
// Implement async iterable
return this;
}
next(){
if(this.promise.length === 0){
this.#generate();
}
return this.promise.shift();
}
callback({ value, done }){
if(this.done){
return;
}
if(this.resolve.length === 0){
this.#generate();
}
// When `value` is `undefined`, `done` should be `true`,
// otherwise default `done` is `false`.
done ??= value === undefined;
this.resolve.shift()({ value, done });
if(done){
this.done = done;
// Clean up all remained promiseses.
for(const r of this.resolve){
r({ done: true });
}
this.resolve = [];
}
}
};
const cb2ag = new Callback2AsyncGenerator();
socket.onmessage = e => {
const value = /* ... */
const done = /* ... */
cb2ag.callback({ value, done });
};
for await (const value of cb2ag){
// ...
}
3. まとめ
イベントハンドラのコールバックAPIを、for await ... of
で使える非同期イテレータに置き換えるためのクラスを実装してみました。
使うことで、設計が良くなるかと言われると微妙に感じるので、もう少し何か良い改善案があればいいなと感じています。
参考
Discussion