/** * 页面卸载清理注册表(Task 89)。 * * 统一管理页面级需在卸载时释放的资源: * - 临时 URL:createObjectURL 产生的 blob URL 通过 revokeUrl 释放; * - 请求:AbortController 在卸载时 abort(registerRequestController); * - timer:dispose 时委托 hooks.clearTimers()(配合 categorized-timers * 的 clearScope 全量清理)。 * * dispose 幂等:第二次及以后的调用不再重复清理;dispose 后注册被拒绝 * 并计数。revokeUrl 抛错不中断其余条目的清理;maxTrackedUrls(默认不 * 限)用于限制同一页面累计注册的 URL 数量,超过时只保留最近注册的。 */ export interface PageCleanupRegistryHooks { /** 卸载时清空全部 timer */ clearTimers: () => void /** 卸载时释放单个 blob URL */ revokeUrl: (url: string) => void } export interface PageCleanupRegistryOptions { /** 累计跟踪的 URL 上限(0 表示不限制),超过时只保留最近注册的 */ maxTrackedUrls?: number } export function createPageCleanupRegistry( hooks: PageCleanupRegistryHooks, options: PageCleanupRegistryOptions = {}, ) { if (typeof hooks?.clearTimers !== 'function') { throw new Error('clearTimers 必须是函数') } if (typeof hooks?.revokeUrl !== 'function') { throw new Error('revokeUrl 必须是函数') } const maxTrackedUrls = options.maxTrackedUrls ?? 0 const urls: string[] = [] const controllers: AbortController[] = [] let disposed = false let disposeCount = 0 let rejectedCount = 0 function registerObjectUrl(url: string): string { if (disposed) { rejectedCount += 1 return url } if (!url) return url urls.push(url) if (maxTrackedUrls > 0 && urls.length > maxTrackedUrls) { urls.splice(0, urls.length - maxTrackedUrls) } return url } function registerRequestController(controller: AbortController) { if (disposed) { rejectedCount += 1 return } controllers.push(controller) } function dispose(): number { if (disposed) { return 0 } disposed = true let released = 0 for (const url of urls) { try { hooks.revokeUrl(url) } catch { /* 单个 URL 释放失败不中断其余清理 */ } released += 1 } urls.length = 0 for (const controller of controllers) { try { controller.abort() } catch { /* abort 失败不中断其余清理 */ } released += 1 } controllers.length = 0 hooks.clearTimers() released += 1 disposeCount += 1 return released } return { get trackedUrlCount() { return urls.length }, get requestCount() { return controllers.length }, get disposed() { return disposed }, get disposeCount() { return disposeCount }, get rejectedCount() { return rejectedCount }, registerObjectUrl, registerRequestController, dispose, } } export type PageCleanupRegistry = ReturnType