インフライト添付アップロードの中止処理を追加
アップロード中の添付ファイルが削除された際に、バックグラウンドで走り続けていた XMLHttpRequest を即座に中止する機構が Lexxy に導入されました。これにより不要なネットワーク通信とリソース消費が防がれ、Docs & Files と同等のユーザー体験が実現します。
背景
Lexxy では、添付ファイルのアップロードを Active Storage の直接アップロード機構で行っていますが、アップロードノードが破棄された場合でもリクエストは残存し、完了かタイムアウトまで待機していました。この挙動は、ユーザーが添付を削除した瞬間にネットワークアクセスが続く点で Docs & Files と動作が不一致でした。
この問題は、アップロードノードが削除されたことを検知できても、実際の XMLHttpRequest オブジェクトへの参照が失われていたために中止できなかったことが原因です。そこで、アップロードリクエストをキーで管理し、破棄時に明示的に abort できる仕組みを追加することが決定されました。
技術的な変更
UploadRequests クラスの導入
新規ファイル src/editor/attachments/upload_requests.js に UploadRequests クラスが追加されました。内部では Map を用いて key → XMLHttpRequest のペアを保持し、track, forget, abort, clear の4メソッドでライフサイクルを管理します。
export class UploadRequests {
#requestsByKey = new Map()
track(key, request) { this.#requestsByKey.set(key, request) }
forget(key) { this.#requestsByKey.delete(key) }
abort(key) {
const request = this.#requestsByKey.get(key)
if (request) {
this.#requestsByKey.delete(key)
request.abort()
}
}
clear() { this.#requestsByKey.clear() }
}
Editor 要素への保持
src/elements/editor.js にプライベートフィールド #uploadRequests が追加され、コンストラクタで new UploadRequests() によりインスタンス化されます。uploadRequests ゲッターで外部から取得できるようにし、リセット時に clear() が呼び出されるよう #reset メソッドに追記しました。
#uploadRequests
get uploadRequests() { return this.#uploadRequests }
connectedCallback() {
// 省略
this.#uploadRequests = new UploadRequests()
// 省略
}
#reset() {
// 省略
this.#uploadRequests?.clear()
// 省略
}
アップロードノードからのトラッキング
src/nodes/action_text_attachment_upload_node.js に #rememberUploadRequest と #forgetUploadRequest のプライベートメソッドが追加され、directUploadWillStoreFileWithXHR デリゲートで取得した request を this.#editorElement.uploadRequests.track(this.getKey(), request) に渡して追跡します。アップロード完了やエラー時に forget が呼び出され、メモリリークを防止します。
#rememberUploadRequest(request) {
this.#editorElement.uploadRequests.track(this.getKey(), request)
}
#forgetUploadRequest() {
this.#editorElement.uploadRequests.forget(this.getKey())
}
添付ノード破棄時の abort 呼び出し
src/extensions/attachments_extension.js の #handleUploadMutations において、mutation === "destroyed" のケースで this.editorElement.uploadRequests.abort(key) が実行されます。これにより、削除された添付に対応するリクエストが即座に中止されます。
for (const [ key, mutation ] of mutations) {
if (mutation === "created") {
this.#uploadsCount++
} else if (mutation === "destroyed") {
this.#uploadsCount--
this.editorElement.uploadRequests.abort(key)
}
}
テストの追加
ユニットテスト として test/javascript/unit/editor/attachments/upload_requests.test.js が追加され、track, abort, forget の正しい振る舞いが検証されています。Playwright 回帰テスト test/browser/tests/attachments/upload_abort_on_delete.test.js では、Active Storage のディスクアップロードを保持した状態で添付を削除し、リクエストが aborted として報告されることを確認しています。
設計判断
キー駆動のリクエスト管理
破棄対象ノードは DOM ミューテーションから得られる唯一のハンドルである「キー」のみで管理できるため、UploadRequests はキーを基点にリクエストを追跡します。この設計は追加のデータ構造を最小限に抑え、エディタインスタンスごとに独立した管理領域を提供します。
Editor 要素への責務委譲
UploadRequests のインスタンスは LexicalEditorElement に保持され、エディタのライフサイクル(接続、リセット)と同期して初期化・クリアされます。これにより、エディタが破棄された際に残存リクエストが残らないことが保証されます。
アップロードノード側の最小侵入
ActionTextAttachmentUploadNode は既存のアップロードロジックに対し、リクエスト取得後に track、完了後に forget を呼び出すだけの追加コードで済みます。これにより、既存のアップロードフローを改変せずに中止機構を組み込むことができ、後方互換性が保たれます。
まとめ
本 PR は、Lexxy の添付アップロードに対し「削除時にリクエストを即時中止」する機構を導入しました。キー駆動の UploadRequests によりリクエストを安全に管理し、エディタとノードのライフサイクルに合わせてトラッキングとクリーンアップを行います。結果として不要なネットワーク通信が防がれ、Docs & Files と同様のユーザー体験が実現されました。