ウェブサイト検索

Typescript/Angular: 順次遅延する非同期タスクの完了を待つ


StackOverflowには似たような質問がいくつもありますが、私はこれを解決できていません。どうかご容赦ください:

Angular 16のアプリケーションで、何百人ものユーザーの名前を一括で変更する機能があります。そのため、RenameUserオブジェクトの配列があり、それぞれにユーザーの名前を変更するAPIコールを実行する非同期メソッド.executeRename()があります。APIを過負荷にしないように、これらは200msの遅延を置いて順番に実行する必要があります。すべてのユーザーの名前変更が完了したら、他のタスクを実行したいです。

要約: 一連の非同期操作を200msの遅延を挟んで開始し、すべて完了したら何かを行う。

私は電話を遅らせることができましたし、1回の電話が終わるのにどれだけ時間がかかっても気にしませんが、すべてのタスクが終了したときには知る必要があります。

これが私が考えた解決策です。プロセスを開始するには、triggerRenameProcess() メソッドを実行します。

async triggerRenameProcess(users: RenameUser[]) {
  try {
    await this.renameUsersWithDelay(users, 200);
    console.log('All users renamed');
  } catch (error) {
    console.error('Exception while renaming users: ', error);
  }
}

renameUsersWithDelay(users: RenameUser[], delay: number): Promise<void> {
  return new Promise<void>((resolve) => {
    let currentIndex = 0;
    const executeNext = () => {
      if (currentIndex < users.length) {
        const user = users[currentIndex];
        user.executeRename().then(() => {
          currentIndex++;
          setTimeout(executeNext, delay);
        });
      } else {
        resolve();
      }
    };
  });
}

問題が何かはなんとなく理解しています。renameUsersWithDelay() は await 可能な Promise を返しますが、それはすべてのリクエストがトリガーされた後に発生するもので、最も時間がかかるものの完了を待つわけではありません。すべてのリクエストを順番に待つようにして順次実行させることもできますが、それは本来の意図ではありません…

私はいくつか調査を行い、この質問やあの質問を見つけましたが、どちらも同じ問題には触れていないようです。そして正直言って、私はJavaScriptにはあまり詳しくありません。.NETの方が慣れていて、そこであれば単に .WaitAll() のようなものを使うでしょう(おそらくより高度なスロットリングパターンを使うかもしれません)が、TypeScriptで同じことをどう実現するかはわかりません。

回答 #1

非自明な非同期データフロー操作がある場合は、オブザーバブルが大いに役立つと考えてよいでしょう。

これが、オブザーバブルで実現しようとしている機能です:

function renameUsersWithDelay(
  users: RenameUser[],
  delayMs: number = 200
): Observable<any> {
  const usersWithDelay = users.map((user, i) =>
    of(user).pipe(delay(delayMs * i), switchMap(executeRename))
  );

  return forkJoin(usersWithDelay);
}

仕組みは次のとおりです:

  • 私たちは users 配列を map でループして、それから新しい配列を作ります
  • 各ユーザーについて、of を使ってユーザーをオブザーバブルに入れます
  • 次に、現在のインデックスに対して遅延を適用するので、それらはすべてシフトされます
  • 一度delayが解決すると、switchMapを使用してHTTP呼び出しを行います。これにより、executeRenameが返すオブザーバブルに実質的にサブスクライブすることになります。
  • オブザーバブルは本質的にコールドであるため、ここで作成したものはまだ何もトリガーされていません。これは素晴らしいことです。なぜなら、トリガーせずにすべての指示を構築することができるからです。
  • 最後に、作成したそれぞれのオブザーバブルにサブスクライブするためにforkJoinを使用します

期待通りに動作することが分かるように、モックデータを使ったライブデモを作成しました。

私はあなたが正確に求めていたものを渡したかったので、それまでにやったことはそういうことです。しかし、私のアドバイスとしては、依然としてバックエンドにDDOSをかけずに、実行中の呼び出しのプールを制限することで、より効率的にする別のアプローチを試してみることです。例えば、進めるものが何も残っていなくなるまで、常に3つのリネーム呼び出しを同時に実行し、現在のプール内の1つが終了したら、キューから別の呼び出しをすぐに取り出して実行する、と言うことです。これは私の意見ではより効率的です。

これを実装する方法は次の通りです:

const renameUsersWithDelay = (users: RenameUser[]): Observable<any> =>
  from(users.map((user) => of(user).pipe(switchMap(executeRename)))).pipe(
    mergeAll(3)
  );

ライブデモ