mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-26 08:51:20 -08:00
refactor(core): CORE-03 add typed resource scopes and cleanup adapters
This commit is contained in:
1 parent
362c616998
commit
3e38dd0d19
12 files changed
+1242
-6
No files matched your search
@@ -48,7 +48,50 @@ installed versions. Candidate corrections have separate published-defect observa
|
||||
in test/public-behavior.test.js and test/browser/emitter.spec.js. The source type fixture
|
||||
checks event name/payload correlation, receiver types, custom channels and chain typing.
|
||||
Emitter owns registrations only; instance teardown must remove the relevant callbacks,
|
||||
not clear unrelated subscribers before destroy dispatch. Resource scopes follow in CORE-03.
|
||||
not clear unrelated subscribers before destroy dispatch. Internal resource scopes are described below.
|
||||
|
||||
## Internal resource ownership
|
||||
|
||||
`src/lifecycle/scope.ts` owns synchronous cleanup registrations. `resources.ts` supplies
|
||||
DOM listener, timeout, animation frame, request-controller and Blob URL adapters;
|
||||
it depends only on the scope type and native browser APIs. Neither file is exported
|
||||
from Artplayer or Artplayer.utils. CORE-03 introduces and tests this boundary;
|
||||
CORE-04 will connect instance construction/destruction. Existing Events, timers and
|
||||
requests have not yet been transferred, so their historical lifecycle findings remain open.
|
||||
|
||||
Create one ResourceScope for an owner and child() for a replaceable operation.
|
||||
Call operation.dispose() on completion or replacement; it detaches from its parent,
|
||||
while sibling operations stay live. Disposing an instance closes all remaining
|
||||
children. add(cleanup) returns an idempotent release function. Registrations are removed
|
||||
before invoking cleanup; disposal closes the scope first and unwinds in reverse order.
|
||||
Late registrations are released immediately. Failures do not interrupt other cleanup:
|
||||
dispose throws one ResourceCleanupError containing the original errors after unwinding.
|
||||
Explicit release throws its own error immediately and is still consumed. Owners must
|
||||
handle these errors at their lifecycle boundary without replacing a primary operation error.
|
||||
|
||||
Cleanup returns undefined, deliberately rejecting async callbacks in strict TS. An
|
||||
asynchronous media/SDK shutdown needs a separately awaited owner protocol; do not cast
|
||||
its Promise to a synchronous disposer. Emitter subscriptions use a per-owner callback
|
||||
and add(() => { emitter.off(name, callback) }); do not remove unrelated subscriptions.
|
||||
|
||||
DOM adapters preserve function receivers and object handleEvent receivers, snapshot
|
||||
capture, and release once/aborted subscriptions. They are internal registrations with
|
||||
their own wrappers, not a replacement for the public Events.proxy identity contract.
|
||||
Timer/RAF callbacks detach before invoking work and guard queued callbacks after
|
||||
cancellation. They allocate nothing when closed. Request controllers are optional on
|
||||
engines lacking AbortController: consumers still need closed/generation checks and
|
||||
must own rejected fetch Promises. Release a completed request's operation scope to
|
||||
avoid retaining its controller. objectURL only accepts a Blob it creates a URL for;
|
||||
it never adopts a caller URL. A closed scope immediately revokes the newly created URL.
|
||||
|
||||
Run `yarn test:unit` and `yarn typecheck` for scope failures, reentry, ownership,
|
||||
controlled timers, queued RAF and type rejection cases. `resource-scope.spec.js` runs
|
||||
an explicitly identified es2015 internal-source fixture in all three Playwright
|
||||
engines with native DOM, RAF, timers, fetch abort and URL access. It is not evidence
|
||||
that the player has adopted scopes or that an installed tarball exposes this API.
|
||||
Fresh fetches test URL revocation; decoded image caches are not a reliable revocation
|
||||
oracle. Keep adapter tests and owner integration tests separate and extend both when
|
||||
moving an existing resource into this boundary.
|
||||
|
||||
## Observable utility behavior
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user