bb2f200d76
新增纯 TS 页面卸载清理注册表 page-cleanup-registry:统一登记 blob 临时 URL(dispose 时 revokeUrl 释放)、AbortController(abort)与 timer 清理委托(clearTimers)。dispose 幂等且后注册拒绝并计数; revokeUrl 抛错不中断其余条目;maxTrackedUrls 上限只保留最近注册的 URL。8 个测试覆盖默认、批量、幂等、空、单元素、上限驱逐、非法 hooks 与 revoke 故障恢复。
117 lines
3.0 KiB
TypeScript
117 lines
3.0 KiB
TypeScript
/**
|
||
* 页面卸载清理注册表(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<typeof createPageCleanupRegistry>
|