/** * @module Drop * * Provides RAII-style resource management for JavaScript using FinalizationRegistry. * Classes extending Drop get automatic cleanup when garbage collected, ensuring * resources are released even if explicitly dropped. * * This is used for managing long-lived resources like health checks, daemons, * and other objects that need cleanup when no longer referenced. */ /** @internal Unique symbol for drop reference identification */ const dropId: unique symbol = Symbol("id") /** @internal Reference type for tracking droppable resources */ export type DropRef = { [dropId]: number } /** * Abstract base class for objects that need cleanup when garbage collected. * * Subclasses must implement `onDrop()` to define cleanup behavior. * The cleanup is automatically triggered when the object is garbage collected, * or can be triggered manually by calling `drop()`. * * @example * ```typescript * class ResourceHolder extends Drop { * private handle: Handle * * constructor() { * super() * this.handle = acquireResource() * } * * onDrop(): void { * releaseResource(this.handle) * } * } * * // Resource is automatically released when holder is garbage collected * let holder = new ResourceHolder() * holder = null // Eventually triggers onDrop() * * // Or manually release * holder.drop() * ``` */ export abstract class Drop { private static weak: { [id: number]: Drop } = {} private static registry = new FinalizationRegistry((id: number) => { const weak = Drop.weak[id] if (weak) weak.drop() }) private static idCtr: number = 0 private dropId?: number private dropRef?: DropRef | WeakRef protected constructor() { this.dropId = Drop.idCtr++ this.dropRef = { [dropId]: this.dropId } const weak = this.weak() Drop.weak[this.dropId] = weak Drop.registry.register(this.dropRef, this.dropId, this.dropRef) return new Proxy(this, { set(target: any, prop, value) { if (prop === "dropRef" || prop == "dropId") return false target[prop] = value ;(weak as any)[prop] = value return true }, }) } protected register() {} private weak(): this { const weak = Object.assign(Object.create(Object.getPrototypeOf(this)), this) if (this.dropRef) weak.ref = new WeakRef(this.dropRef) return weak } abstract onDrop(): void drop(): void { if (!this.dropRef || !this.dropId) return this.onDrop() this.leak() } leak(): this { if (!this.dropRef || !this.dropId) return this Drop.registry.unregister(this.dropRef) delete Drop.weak[this.dropId] delete this.dropRef delete this.dropId return this } } export class DropPromise implements Promise { private static dropFns: { [id: number]: () => void } = {} private static registry = new FinalizationRegistry((id: number) => { const drop = DropPromise.dropFns[id] if (drop) { drop() delete DropPromise.dropFns[id] } }) private static idCtr: number = 0 private dropId: number private dropRef: DropRef; [Symbol.toStringTag] = "DropPromise" private constructor( private readonly promise: Promise, dropFnOrRef?: (() => void) | DropRef, ) { if (dropFnOrRef && dropId in dropFnOrRef) { this.dropId = dropFnOrRef[dropId] this.dropRef = dropFnOrRef return } this.dropId = DropPromise.idCtr++ this.dropRef = { [dropId]: this.dropId } if (dropFnOrRef) DropPromise.dropFns[this.dropId] = dropFnOrRef DropPromise.registry.register(this.dropRef, this.dropId, this.dropRef) } static of(promise: Promise, dropFn?: () => void): DropPromise { return new DropPromise(promise, dropFn) } static ref(promise: Promise, dropRef: DropRef): DropPromise { return new DropPromise(promise, dropRef) } then( onfulfilled?: | ((value: T) => TResult1 | PromiseLike) | null | undefined, onrejected?: | ((reason: any) => TResult2 | PromiseLike) | null | undefined, ): Promise { return DropPromise.ref( this.promise.then(onfulfilled, onrejected), this.dropRef, ) } catch( onrejected?: | ((reason: any) => TResult | PromiseLike) | null | undefined, ): Promise { return DropPromise.ref(this.promise.catch(onrejected), this.dropRef) } finally(onfinally?: (() => void) | null | undefined): Promise { return DropPromise.ref(this.promise.finally(onfinally), this.dropRef) } } export class DropGenerator implements AsyncGenerator { private static dropFns: { [id: number]: () => void } = {} private static registry = new FinalizationRegistry((id: number) => { const drop = DropGenerator.dropFns[id] if (drop) { drop() delete DropGenerator.dropFns[id] } }) private static idCtr: number = 0 private dropId: number private dropRef: DropRef; [Symbol.asyncIterator] = () => this private constructor( private readonly generator: AsyncGenerator, dropFn?: () => void, ) { this.dropId = DropGenerator.idCtr++ this.dropRef = { [dropId]: this.dropId } if (dropFn) DropGenerator.dropFns[this.dropId] = dropFn DropGenerator.registry.register(this.dropRef, this.dropId, this.dropRef) } static of( generator: AsyncGenerator, dropFn?: () => void, ): DropGenerator { return new DropGenerator(generator, dropFn) } next(...args: [] | [TNext]): Promise> { return DropPromise.ref(this.generator.next(...args), this.dropRef) } return( value: TReturn | PromiseLike, ): Promise> { return DropPromise.ref(this.generator.return(value), this.dropRef) } throw(e: any): Promise> { return DropPromise.ref(this.generator.throw(e), this.dropRef) } }