エクステンションに `dispose()` ライフサイクルフックを追加してリソースリークを解消

basecamp/lexxy

Lexxyのエクステンション基底クラスに dispose() メソッドが追加され、エディタの切断・再接続時にリスナーやタイマーなどのリソースを確実にクリーンアップできるようになりました。Turboなどのナビゲーションによる back/forward 移動で発生していた重複ハンドラ問題が根本的に解消されます。

背景

エディタ要素の connectedCallback が呼ばれるたびに新しい Extensions インスタンスが生成されていましたが、旧インスタンスが保持するリソースを解放する手段がありませんでした。initializeToolbar() でツールバーに追加したリスナーは、エディタが DOM から切り離された後も生き続け、次の connectedCallback で再度登録されるため、N 回の再接続後にハンドラが N 回発火するという問題が生じていました。

この問題が特に顕在化するのは、Turbo の back/forward ナビゲーションのケースです。Turbo はツールバーの DOM を切断・再接続のサイクルをまたいで保持するため、エクステンションが initializeToolbar() でツールバーに付与したリスナーが再接続のたびに蓄積されていきました。

技術的な変更

変更は4つのファイルにまたがり、ライフサイクルフックの宣言・集約呼び出し・エディタへの登録という3段階の構造になっています。

LexxyExtension への no-op メソッド追加として、src/extensions/lexxy_extension.js の基底クラスに空の dispose() が定義されました。サブクラスは必要に応じてオーバーライドするだけでよく、実装しないエクステンションは何もしない既定の振る舞いをそのまま継承します。

dispose() {
}

Extensions.dispose() による逆順クリーンアップとして、src/editor/extensions.js には while ループで pop() しながら各エクステンションの dispose() を呼ぶメソッドが追加されました。

dispose() {
  while (this.enabledExtensions.length) {
    this.enabledExtensions.pop().dispose()
  }
}

pop() を使うことで、初期化の逆順でクリーンアップが走ります。依存関係のあるエクステンション同士が正しい順序で解体されるよう配慮された実装です。

エディタへの disposables 登録として、src/elements/editor.js では this.#disposables.push(this.extensions) の1行が追加されました。これにより、エディタ本体やリスナーと同じタイミングで Extensions.dispose() が自動的に呼ばれるようになり、呼び出し側が意識する必要はありません。

this.extensions = new Extensions(this)
this.#disposables.push(this.extensions)  // 追加

this.editor = this.#createEditor()
this.#disposables.push(this.editor)

設計判断

既存の #disposables パターンへの統合という方針が採用されました。エディタ要素はすでにエディタ本体やリスナーを #disposables 配列で管理しており、Extensions インスタンスをその配列に追加するだけで済む設計になっています。新たなライフサイクル管理の仕組みを設けず、既存のパターンを再利用した判断です。

AbortController を活用したテストフィクスチャも注目に値します。追加されたブラウザテスト (test/browser/fixtures/extension-dispose.js) では、#abortControllersignaladdEventListener に渡すことで、dispose() 内の abort() 一呼び出しですべてのリスナーを一括解除しています。これはエクステンション実装のリファレンスパターンとして機能します。

class DisposableListenerExtension extends Extension {
  #abortController = new AbortController()

  initializeToolbar(toolbar) {
    toolbar.addEventListener(
      "lexxy-test-ping",
      () => { window.__lexxyDisposeTest_pingCount = (window.__lexxyDisposeTest_pingCount || 0) + 1 },
      { signal: this.#abortController.signal }
    )
  }

  dispose() {
    window.__lexxyDisposeTest_disposeCount = (window.__lexxyDisposeTest_disposeCount || 0) + 1
    this.#abortController.abort()
  }
}

ブラウザテストは「切断→再接続後に dispose() が1回呼ばれること」「ping イベントが再接続後も1回だけカウントされること」の2点を検証しており、リグレッションを防ぐ安全網となっています。

まとめ

dispose() ライフサイクルフックの追加は、Turbo などのナビゲーションを前提とした SPA 環境でエクステンションを安全に運用するための基盤となる変更です。既存の #disposables パターンへの統合により、エクステンション開発者は dispose() をオーバーライドするだけでリソース解放を正しいタイミングに委ねられるようになりました。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
e95aceb2

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)→セクション群(各論)→まとめ(結論)の3部構成が明確に適用されており、記事全体の構成が非常に分かりやすいです。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```javascript:filepath)やPR番号のリンク記法が正しく使用されており、可読性が高いです。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

「connectedCallback」「disposablesパターン」などの専門用語を前提として説明が進められており、対象読者である専門知識を持つエンジニアに適合しています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論で構成され、各パラグラフもトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が守られており、論理的で読みやすいです。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内で引用されているすべてのコードブロックは、提供されたDiffの内容と正確に一致しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`dispose()`, `no-op`, `AbortController` などの技術用語が文脈に応じて正確かつ適切に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

「初期化の逆順でのクリーンアップ」や「既存の`#disposables`パターンへの統合」など、コード変更の意図や仕組みに関する説明が技術的に正確かつ論理的です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張は、PRのDescriptionやDiffの内容によって裏付けられており、ハルシネーション(創作)は一切見られません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#1029)やファイルパスが正確に記載されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトルはPRのタイトル「Add dispose() lifecycle hook to extensions」の内容を正確に反映し、その目的(リソースリーク解消)も補足しており適切です。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

PR情報に含まれないバージョン情報やリリース予定などの外部知識の持ち込みはなく、事実に忠実な内容です。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「既に」「近い将来」といった時間表現の歪曲はなく、事実関係が正確に記述されています。