非同期イベントの終了処理は流入停止→drain→リソース解放の順にする
イベントリスナーが発火するたびに非同期処理を開始し、その Promise を配列へ追加する設計では、終了時にその配列へ一度だけ Promise.all 相当を適用しても「終了待ち」にはならない。集約処理が参照するのは呼び出し時点の要素であり、その後にイベントが追加した Promise は対象外だからである。リソースの close 自体が追加イベントを発生させる場合、スクリーンショットなどの副作用が閉じたリソースへ走る、収集結果の確定後に配列へ追加されてレポートから欠落する、といった非決定的な不整合になる。
適用条件
ブラウザやソケットのイベント監視、ログ・メトリクス収集、ファイル監視など、イベントハンドラ内で非同期 I/O を行い、最後に成果物を確定する処理に当てはまる。特に teardown が新しい失敗・close イベントを起こし得る系では重要になる。
安全な終了順序
まず新規イベントの流入を止める。リスナー解除、終了フラグによる受付停止、またはイベントキューの close を使う。次に、受付済みの非同期処理がゼロになるまで drain する。単なる配列スナップショットではなく、in-flight カウンタや明示的なタスク集合を使い、追加不能になった状態で完了を待つ。その後にページ・コンテキスト・ソケット等を閉じ、最後に収集結果と成果物を確定する。close 中のイベントも記録対象にする要件なら、close 用の収集フェーズを別に設け、そこでも流入停止と drain の境界を明示する。
検証方法
非同期ハンドラを意図的に遅延させた状態で終了処理を開始し、終了直前と close 中にもイベントを発生させる。成果物の確定が全受付済みタスクの完了後であること、閉じたリソースに副作用が走らないこと、イベント件数と成果物件数が一致することを確認する。通常の純粋関数テストだけではこの競合を検知できないため、制御可能な遅延を入れたライフサイクルテストが必要になる。