fix(tiptap): clean up pending upload on NodeView.destroy to prevent blob leak

This commit is contained in:
xfy 2026-06-23 18:36:47 +08:00
parent 2e239908a4
commit 6b44a9c036

View File

@ -2,10 +2,12 @@ import { Image } from '@tiptap/extension-image'
import type { Node as PMNode } from '@tiptap/pm/model' import type { Node as PMNode } from '@tiptap/pm/model'
import type { UploadCoordinator } from './upload-coordinator' import type { UploadCoordinator } from './upload-coordinator'
/** NodeView 按钮点击回调注入接口。 */ /** NodeView 按钮点击 / destroy 回调注入接口。 */
export interface UploadNodeViewCallbacks { export interface UploadNodeViewCallbacks {
onRetry: (uploadId: string) => void onRetry: (uploadId: string) => void
onRemove: (uploadId: string) => void onRemove: (uploadId: string) => void
/** NodeView.destroy 兜底:节点被 PM 删除时清理 pending + revoke blob。 */
onDestroyed: (uploadId: string) => void
} }
/** /**
@ -22,6 +24,7 @@ export interface UploadNodeViewCallbacks {
class UploadImageNodeView { class UploadImageNodeView {
private node: PMNode private node: PMNode
private callbacks: UploadNodeViewCallbacks private callbacks: UploadNodeViewCallbacks
private uploadId: string | null
private container: HTMLDivElement private container: HTMLDivElement
private img: HTMLImageElement private img: HTMLImageElement
@ -34,6 +37,7 @@ class UploadImageNodeView {
}) { }) {
this.node = opts.node this.node = opts.node
this.callbacks = opts.callbacks this.callbacks = opts.callbacks
this.uploadId = (this.node.attrs['data-upload-id'] as string | null) ?? null
this.container = document.createElement('div') this.container = document.createElement('div')
this.container.classList.add('upload-image-container') this.container.classList.add('upload-image-container')
@ -66,6 +70,7 @@ class UploadImageNodeView {
const oldSrc = this.node.attrs.src const oldSrc = this.node.attrs.src
const newSrc = node.attrs.src const newSrc = node.attrs.src
this.node = node this.node = node
this.uploadId = (node.attrs['data-upload-id'] as string | null) ?? null
if (oldSrc !== newSrc && newSrc != null) { if (oldSrc !== newSrc && newSrc != null) {
this.img.src = newSrc this.img.src = newSrc
} }
@ -130,6 +135,11 @@ class UploadImageNodeView {
} }
destroy(): void { destroy(): void {
// 节点被 ProseMirror 删除(退格/剪切等):兜底清理 pending + revoke blob
// 避免 upload-coordinator 的 pending Map 和 blob URL 泄漏(尤其 error 态节点)。
if (this.uploadId) {
this.callbacks.onDestroyed(this.uploadId)
}
this.overlay?.remove() this.overlay?.remove()
this.overlay = null this.overlay = null
this.container.remove() this.container.remove()