ObservablePromiseはAsyncIterableインターフェイスを実装することで途中状態を取得可能にしたPromiseである。ObservablePromiseの最大の利点は非同期の途中状態と最終状態という意味的に異なる状態の分離をfor-awaitとasync/awaitという言語抽象レベルで行うことで標準的な形でこれを区別して扱えることであり、その内部表現もまた言語抽象であるAsync generatorで標準にのっとり簡潔に記述できる。
ObservablePromise抽象はコルーチンと親和性が高いためここではObservablePromiseをコルーチンとして実装した。ただしコルーチンの操作的基本機能であるsuspend/resumeはJavaScriptでは実用性が低いためプロパティを通してポートオブジェクトの形で追加的に提供する*1。ObservablePromiseは次のような表現を可能にする。なおこのコルーチンはCancelableでもあるためより正確にはCancelableObservablePromiseであり、単なるCancelablePromiseとしても有用性が高い。また基底のPromiseには同期Promiseを使用しておりコルーチンの生死判定とその事後処理は遅延なく同期的に行われる。
it('terminate', done => {
let cnt = 0;
const co = new Coroutine(async function* () {
assert(cnt === 0 && ++cnt);
return 0;
});
co.finally(() => {
assert(cnt === 1 && ++cnt);
});
co[Coroutine.terminator]();
assert(cnt === 2 && ++cnt);
co[Coroutine.terminator](1);
co.catch(done);
});
it('iterate', async () => {
let cnt = 0;
const co = new Coroutine<number, number>(async function* () {
assert(++cnt === 1);
assert(undefined === (yield Promise.resolve(2)));
assert(undefined === (yield Promise.resolve(3)));
await wait(100);
assert(undefined === (yield Promise.resolve(4)));
return Promise.resolve(5);
});
assert(cnt === 1);
for await (const n of co) {
assert(n === ++cnt);
}
for await (const _ of co) {
assert(false);
}
assert(await co === ++cnt);
assert(cnt === 5);
});
https://github.com/falsandtru/spica/blob/master/src/coroutine.test.ts
XMLHttpRequestはObservablePromiseで表現することで非常に簡潔になる。
it('basic', async () => {
const co = cofetch('');
for await (const ev of co) {
assert(ev instanceof ProgressEvent);
assert(['loadstart', 'progress', 'loadend'].includes(ev.type));
}
assert(await co instanceof XMLHttpRequest);
});
https://github.com/falsandtru/spica/blob/master/src/cofetch.test.ts
このようにObservablePromiseは優れた抽象表現でありその標準性から一般に推奨できるものである。ただしNode.jsが非同期APIにPromiseを採用しなかったようにパフォーマンス上の問題が生じる場合はこの限りではない。また2018年12月時点ではEdgeでAsyncIteratorが未実装であるためブラウザではもうしばらく待つ必要がある。そのほか、自身はコルーチンの管理にSupervisorを使用してプロセス管理の煩雑さを低減している。そしてこのSupervisorもまたコルーチンである。