setTimeout
関数 はコールバック方式であり使い勝手が悪いので
これを Promise
化する。
キャンセルできるようにもする。
準備:型定義
Promise
オブジェクトとキャンセル関数を次のようなオブジェクトにまとめて返してもよいが、
{
promise: Promise,
cancel: () => void,
}
これだと使う際にひと手間かかる。
そこで、Promise
に cancel()
メソッドを追加した CancellablePromise
型を定義し、
その型のオブジェクトを返すことにする。
こうすれば、呼び出し側でキャンセルを使わない場合には単なる Promise
として扱うことができる。
/**
* キャンセル可能なオブジェクトのインターフェイス。
*/
interface Cancellable {
cancel(): void;
}
/**
* キャンセル可能な Promise。
*
* キャンセルするためのメソッド cancel() を持つ。
* cancel() を呼び出すと Promise が reject される。
* このときの reason には Cancelled クラスのインスタンスが渡される。
*/
type CancellablePromise<T> = Promise<T> & Cancellable;
オブジェクトに cancel
メソッドを追加する際にはどうしても any
型へのキャストが必要になる。
それを隠蔽する関数を定義する。
/**
* 指定されたオブジェクトに cancel メソッドを追加して Cancellable にする。
*
* @param base Cancellable にするオブジェクト。
* @param cancel cancel メソッドとして base のメンバに追加する関数。
*/
function toCancellable<T>(
base: T,
cancel: () => void
): T & Cancellable {
const baseAsAny = base as any;
baseAsAny.cancel = cancel;
return baseAsAny;
}
CancellablePromise
がキャンセルされたときには、キャンセルされずにタイムアウトしたときと区別できるよう、reject
することにしたい。
キャンセルによる reject
は、それ以外の理由による reject
と区別できるようにしたい。
そのため、キャンセルによる reject
での reason として使うクラスを定義する。
/**
* CancellablePromise がキャンセルされたときに
* reject の reason として渡されるオブジェクトのクラス。
*/
class CancellablePromiseCancelled { }
関数実装
/**
* 指定された時間の後に resolve される、キャンセル可能な Promise を生成して返す。
*
* @param delay 返した Promise が resolve されるまでの時間[ミリ秒]。
*/
function timeoutAsync(delay: number): CancellablePromise<void> {
// setTimeout 関数が返した ID。
// 指定時間が経過するかキャンセルされたら null。
let timeoutID: number | null = null;
let rejectPromise: (reason: CancellablePromiseCancelled) => void
= (_) => { }; // non-nullable 型にするため、ダミーの null オブジェクトで初期化する。
const promise = new Promise<void>((resolve, reject) => {
timeoutID = setTimeout(() => {
timeoutID = null;
resolve();
}, delay);
rejectPromise = reject;
});
const cancel = () => {
if (timeoutID !== null) {
clearTimeout(timeoutID);
timeoutID = null;
rejectPromise(new CancellablePromiseCancelled());
}
};
return toCancellable(promise, cancel);
}
動作確認
キャンセルなし
(async () => {
const beginAt = performance.now();
await timeoutAsync(1_000);
const endAt = performance.now();
console.log(`${endAt - beginAt}`); // 約 1,000 が出力される。
})();
約 1,000 の数値が出力されることから、正常に動作していることが確認できる。
キャンセルあり
(async () => {
// 2秒後に resolve される Promise を生成する。
const promise = timeoutAsync(2_000);
// 1秒経過した時点でキャンセルする。
(async () => {
await timeoutAsync(1_000);
promise.cancel();
})();
try {
await promise;
} catch (e) {
if (e instanceof CancellablePromiseCancelled) {
console.log("promise はキャンセルされました。"); // > promise はキャンセルされました。
}
}
})();
2秒後に resolve
される Promise
を生成し、1秒経過した時点でそれをキャンセルしている。
キャンセルされた Promise
は、それを await
した際に CancellablePromiseCancelled
オブジェクトをスローする。
promise はキャンセルされました。
と出力されることから、正常に動作していることが確認できる。
補足
CancellablePromiseCancelled
クラスに、どの関数で生成した CancellablePromise
がキャンセルされたのかなどが分かるよう情報を追加してもよいだろう。
派生クラスで追加してもよい。
/以上