task-89: 清理页面卸载时的所有 timer、请求和临时 URL

新增纯 TS 页面卸载清理注册表 page-cleanup-registry:统一登记 blob
临时 URL(dispose 时 revokeUrl 释放)、AbortController(abort)与
timer 清理委托(clearTimers)。dispose 幂等且后注册拒绝并计数;
revokeUrl 抛错不中断其余条目;maxTrackedUrls 上限只保留最近注册的
URL。8 个测试覆盖默认、批量、幂等、空、单元素、上限驱逐、非法 hooks
与 revoke 故障恢复。
This commit is contained in:
2026-08-30 22:57:04 +08:00
parent 79961170c3
commit bb2f200d76
2 changed files with 270 additions and 0 deletions
@@ -0,0 +1,116 @@
/**
* 页面卸载清理注册表(Task 89)。
*
* 统一管理页面级需在卸载时释放的资源:
* - 临时 URLcreateObjectURL 产生的 blob URL 通过 revokeUrl 释放;
* - 请求:AbortController 在卸载时 abortregisterRequestController);
* - timerdispose 时委托 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<typeof createPageCleanupRegistry>