Deno 標準ライブラリ async

先日、DenoDBのモジュールの中身を調べていた。
調べていたら Deno の標準ライブラリのasyncに突き当たった。
サンプルを見てピンと来なかったものがあったので、動作確認してみた。

参考

確認

delay

promise 化した setTimeout のようになっている様子。

1
2
3
4
5
6
7
8
9
import { delay } from "https://deno.land/std/async/mod.ts";

const startTime = Date.now();

await delay(2000);

let time = Date.now();
console.log(time - startTime);
// => 2002

中身を見ると、AbortSignal 対応していたので、自前で setTimeout を Promise 化するよりこちら使う方がよさげ。

debounce ?

デバウンス?
もともと電気電子系の用語らしい。
npm で公開されているlodashにも同じ名前で公開された関数があるそう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { debounce, delay } from "https://deno.land/std/async/mod.ts";

let func = debounce((value: string) => console.log(`func(${value})`), 2000);

const startTime = Date.now();

let time = Date.now();
console.log(time - startTime);
// => 0
func("f1");

await delay(1000);

time = Date.now();
console.log(time - startTime);
// => 1003
func("f2");

await delay(3000); // <= 3000 秒の待ちが発生した結果最後に実行した func("f2"); が実行されている。
// => func(f2)

time = Date.now();
console.log(time - startTime);
// => 4004

func("f3");

await delay(1000);

time = Date.now();
console.log(time - startTime);
// => 5005
func("f4");
// => func(f4)

debounce の第 2 引数で設定した時間以内に呼び出しされた場合には実行されない。
一定時間以内に呼び出しが無ければ第一引数で設定した関数が、最後に呼び出したときの引数に基づいて実行される。

「イベント処理の間引きに使う」という意味ピンと来なかったが動かしてみて意味がわかった。

deferred ?

deferred は、英語で「延期」を意味する。
jQuery にも同様の名称の関数があった。

サンプル参考に確認。

1
2
3
4
5
import { deferred } from "https://deno.land/std/async/mod.ts";

const p = deferred<number>();

p.resolve(42);

何もわからない。
ということで、もう少し書いてみる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { deferred, type Deferred } from "https://deno.land/std/async/mod.ts";

const p = deferred<number>();
// ...

const func = async (d: Deferred<number>) => {
console.log("Wait start"); // d.resolve()されるまで待つ
const src = await d;
console.log("Wait end");
console.log(src);
};

console.log("A");

func(p);
console.log("B");

p.resolve(42);
console.log("C");

実行結果は次のようになる。

1
2
3
4
5
6
7
$ deno run async.ts
A
Wait start
B
C
Wait end
42

ここまで作って理解。
「延期」の名の通り、deferred() を引数に渡して実際の引数設定を延期させることができた。

ちなみに、p.reject(42); と書くと error: Uncaught (in promise) とエラーになる。

MuxAsyncIterator

複数のストリームを、多重化できる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { MuxAsyncIterator } from "https://deno.land/std/async/mod.ts";

async function* gen123(): AsyncIterableIterator<number> {
yield 1;
yield 2;
yield 3;
}

async function* gen456(): AsyncIterableIterator<number> {
yield 4;
yield 5;
yield 6;
}

const mux = new MuxAsyncIterator<number>();
mux.add(gen123());
mux.add(gen456());

for await (const value of mux) {
console.log(value);
}

実行結果は次の通り。

1
2
3
4
5
6
7
$ deno run async.ts
1
4
2
5
3
6

2つのストリームの実行結果が交互に混ざった形で、取得できる
多重化?と感じたが交互に出てくるということだった。
3 つなら 3 つ多重化できた。

pooledMap

(非同期の)イテレーターを、非同期のイテレータに変換できる関数。

サンプルを見るとピンとこないので、先にソースを見る。

pooledMap の実装から抜粋
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* pooledMap transforms values from an (async) iterable into another async
* iterable. The transforms are done concurrently, with a max concurrency
* defined by the poolLimit.
*
* If an error is thrown from `iterableFn`, no new transformations will begin.
* All currently executing transformations are allowed to finish and still
* yielded on success. After that, the rejections among them are gathered and
* thrown by the iterator in an `AggregateError`.
*
* @param poolLimit The maximum count of items being processed concurrently.
* @param array The input array for mapping.
* @param iteratorFn The function to call for every item of the array.
*/

export function pooledMap<T, R>(
poolLimit: number,
array: Iterable<T> | AsyncIterable<T>,
iteratorFn: (data: T) => Promise<R>
): AsyncIterableIterator<R> {
// ...
}

これは正直サンプル見ただけでは、意味不明。
第二引数に渡したイテレータを、第一引数にで定義した値だけ同時に処理(おそらく遅延なく連続して実行が正しい?)し、
第三引数で定義した関数の記述に基づいて新しい非同期イテレーターとしてふるまうようです。

ここでサンプルを確認。

1
2
3
4
5
6
7
8
9
10
11
import { pooledMap } from "https://deno.land/std/async/mod.ts";

const results = pooledMap(
2,
[1, 2, 3],
(i) => new Promise((r) => setTimeout(() => r(i), 1000))
);

for await (const value of results) {
console.log(value);
}

実行すると次の通り。

1
2
3
4
$ deno run async.ts
1
2 <=ここまで連続して処理され、1秒停止
3

一定間隔で処理しつつ、その間隔で複数個処理するパターンが組めるようで有れば便利に使えそうです。
例えば、数秒に 1 回 3 種類のパラメータで API にアクセスするとか。

tee

シェルコマンドの tee をイメージしたが、意味合いとしては同じくしていた。
ストリームの出力先を分割できた。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { tee } from "https://deno.land/std/async/tee.ts";

const gen = async function* gen() {
yield 1;
yield 2;
yield 3;
};

const [branch1, branch2] = tee(gen());

await (async () => {
for await (const n of branch1) {
console.log(n);
}
})();

await (async () => {
for await (const n of branch2) {
console.log(n);
}
})();

実行すると次のようになる。

1
2
3
4
5
6
1
2
3
1
2
3

ループに書いてある、トップレベル await を外すと、2つの処理での出力が交錯する。

ジェネレーターで作成された値を、複数の処理で使いたい。
が、その複数の処理自体には直接関連性がない時、同じループで処理するのにモヤっとしてた。
今後これは使いそう。(時刻とかが絡まなければ)

deadline

設定した時間以内に処理できない場合エラーを起こすことが deadline。

1
2
3
4
5
6
7
8
9
10
import { deadline, delay } from "https://deno.land/std/async/mod.ts";

const delayedPromise = async () => {
await delay(1000);
return 10;
};

const result = await deadline(delayedPromise(), 10);

console.log(result);

実行すると次のようになる。

1
2
3
4
5
6
7
$ deno run async.ts
error: Uncaught (in promise) DeadlineError: Deadline
const t = setTimeout(() => d.reject(new DeadlineError()), delay);
^
at https://deno.land/std@0.123.0/async/deadline.ts:15:39
at Object.action (deno:ext/timers/01_timers.js:161:11)
at handleTimerMacrotask (deno:ext/timers/01_timers.js:78:12)

第二引数 10 = 10ms 以内に、第一引数の結果が受け取れなかったからエラーになったというもの
時間を伸ばすと、resultに結果が返ってくるようになる。

1
2
3
4
5
6
7
8
9
10
11
import { deadline, delay } from "https://deno.land/std/async/mod.ts";

const delayedPromise = async () => {
await delay(1000);
return 10;
};

const result = await deadline(delayedPromise(), 2000);

console.log(result);
// => 10

throw されているので、エラー処理はもちろんできる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { deadline, delay } from "https://deno.land/std/async/mod.ts";

const delayedPromise = async () => {
await delay(1000);
return 10;
};

let result: undefined|number

try {
result = await deadline(delayedPromise(), 10);
} catch (e) {
console.log(e);
// => DeadlineError: Deadline
// at https://deno.land/std@0.123.0/async/deadline.ts:15:39
// at Object.action (deno:ext/timers/01_timers.js:161:11)
// at handleTimerMacrotask (deno:ext/timers/01_timers.js:78:12)
result = 0;
}

console.log(result);
// => 0

というわけで、Deno の async のモジュールを一通り見てみた。
delayを筆頭に、存在を知らないとクオリティーを下げた自作してしまうようなものが多かったと感じる。

ではでは。