refactor(core): CORE-03 add typed resource scopes and cleanup adapters

This commit is contained in:
Harvey Zhao committed 2026-09-11 00:21:32 +08:00
1 parent 362c616998
commit 3e38dd0d19
12 files changed
+1242 -6

No files matched your search

@@ -0,0 +1,85 @@
import type ResourceScope from './scope'
export function listen(scope: ResourceScope, target: EventTarget, name: string, callback: EventListenerOrEventListenerObject, options: AddEventListenerOptions = {}): () => void {
const { capture = false, once = false, signal } = options
if (scope.closed || signal?.aborted)
return () => {}
let release = () => {}
function listener(this: EventTarget, event: Event) {
if (once)
release()
if (scope.closed)
return
if (typeof callback === 'function')
callback.call(this, event)
else
callback.handleEvent(event)
}
target.addEventListener(name, listener, options)
release = scope.add(() => {
try {
target.removeEventListener(name, listener, capture)
}
finally {
signal?.removeEventListener('abort', release)
}
})
signal?.addEventListener('abort', release, { once: true })
return release
}
export function timeout(scope: ResourceScope, callback: () => void, delay: number): () => void {
if (scope.closed)
return () => {}
let pending = true
let release = () => {}
const timer = setTimeout(() => {
if (!pending)
return
release()
if (!scope.closed)
callback()
}, delay)
release = scope.add(() => {
pending = false
clearTimeout(timer)
})
return release
}
export function animationFrame(scope: ResourceScope, callback: FrameRequestCallback): () => void {
if (scope.closed)
return () => {}
let pending = true
let release = () => {}
const frame = requestAnimationFrame((time) => {
if (!pending)
return
release()
if (!scope.closed)
callback(time)
})
release = scope.add(() => {
pending = false
cancelAnimationFrame(frame)
})
return release
}
export function requestController(scope: ResourceScope): AbortController | undefined {
if (typeof AbortController === 'undefined')
return undefined
const controller = new AbortController()
scope.add(() => {
controller.abort()
})
return controller
}
export function objectURL(scope: ResourceScope, blob: Blob): string {
const url = URL.createObjectURL(blob)
scope.add(() => {
URL.revokeObjectURL(url)
})
return url
}
+65
View File
@@ -0,0 +1,65 @@
export type Cleanup = () => undefined
export class ResourceCleanupError extends Error {
constructor(readonly errors: unknown[]) {
super('Failed to release ArtPlayer resources')
this.name = 'ResourceCleanupError'
}
}
export default class ResourceScope {
private cleanups = new Set<() => void>()
private disposed = false
get closed(): boolean {
return this.disposed
}
add(cleanup: Cleanup): () => void {
let active = true
const release = () => {
if (!active)
return
active = false
this.cleanups.delete(release)
cleanup()
}
if (this.closed)
release()
else
this.cleanups.add(release)
return release
}
child(): ResourceScope {
const child = new ResourceScope()
const release = this.add(() => {
child.dispose()
})
child.add(() => {
release()
})
return child
}
dispose(): void {
if (this.closed)
return
this.disposed = true
const errors: unknown[] = []
// Unwind dependencies; release removes each registration before invoking it.
for (const release of Array.from(this.cleanups).reverse()) {
try {
release()
}
catch (error) {
if (error instanceof ResourceCleanupError)
errors.push(...error.errors)
else
errors.push(error)
}
}
if (errors.length)
throw new ResourceCleanupError(errors)
}
}