UNPKG

@difizen/mana-core

Version:

336 lines (283 loc) 8.57 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Iterable } from '@difizen/mana-common'; import { injectable } from '@difizen/mana-syringe'; export function once<T extends (...args: any[]) => any>(this: unknown, fn: T): T { // eslint-disable-next-line @typescript-eslint/no-this-alias const _this = this; let didCall = false; let result: unknown; return function () { if (didCall) { return result; } didCall = true; // eslint-disable-next-line prefer-rest-params result = fn.apply(_this, [...arguments]); return result; } as unknown as T; } /** * Enables logging of potentially leaked disposables. * * A disposable is considered leaked if it is not disposed or not registered as the child of * another disposable. This tracking is very simple an only works for classes that either * extend VSDisposable or use a DisposableStore. This means there are a lot of false positives. */ const TRACK_DISPOSABLES = false; let disposableTracker: IDisposableTracker | null = null; export interface IDisposableTracker { trackDisposable: (x: IDisposable) => void; markTracked: (x: IDisposable) => void; } export function setDisposableTracker(tracker: IDisposableTracker | null): void { disposableTracker = tracker; } if (TRACK_DISPOSABLES) { const __is_disposable_tracked__ = '__is_disposable_tracked__'; disposableTracker = new (class implements IDisposableTracker { trackDisposable(x: IDisposable): void { const stack = new Error('Potentially leaked disposable').stack!; setTimeout(() => { if (!(x as any)[__is_disposable_tracked__]) { console.warn(stack); } }, 3000); } markTracked(x: IDisposable): void { // eslint-disable-next-line @typescript-eslint/no-use-before-define if (x && x !== VSDisposable.None) { try { (x as any)[__is_disposable_tracked__] = true; } catch { // noop } } } })(); } function markTracked<T extends IDisposable>(x: T): void { if (!disposableTracker) { return; } disposableTracker.markTracked(x); } export function trackDisposable<T extends IDisposable>(x: T): T { if (!disposableTracker) { return x; } disposableTracker.trackDisposable(x); return x; } export class MultiDisposeError extends Error { public readonly errors: any[]; constructor(errors: any[]) { super( `Encountered errors while disposing of store. Errors: [${errors.join(', ')}]`, ); this.errors = errors; } } export interface IDisposable { dispose: () => void; } export function isDisposable<E extends object>(thing: E): thing is E & IDisposable { return ( typeof (<IDisposable>thing).dispose === 'function' && (<IDisposable>thing).dispose.length === 0 ); } export function dispose<T extends IDisposable>(disposable: T): T; export function dispose<T extends IDisposable>( disposable: T | undefined, ): T | undefined; export function dispose< T extends IDisposable, A extends IterableIterator<T> = IterableIterator<T>, >(disposables: IterableIterator<T>): A; export function dispose<T extends IDisposable>(disposables: T[]): T[]; export function dispose<T extends IDisposable>(disposables: readonly T[]): readonly T[]; export function dispose<T extends IDisposable>( arg: T | IterableIterator<T> | undefined, ): any { if (Iterable.is(arg)) { const errors: any[] = []; for (const d of arg) { if (d) { markTracked(d); try { d.dispose(); } catch (e) { errors.push(e); } } } if (errors.length === 1) { throw errors[0]; } else if (errors.length > 1) { throw new MultiDisposeError(errors); } return Array.isArray(arg) ? [] : arg; } if (arg) { markTracked(arg); arg.dispose(); return arg; } } export function combinedDisposable(...disposables: IDisposable[]): IDisposable { disposables.forEach(markTracked); return toDisposable(() => dispose(disposables)); } export function toDisposable(fn: () => void): IDisposable { const self = trackDisposable({ dispose: () => { markTracked(self); fn(); }, }); return self; } export class DisposableStore implements IDisposable { static DISABLE_DISPOSED_WARNING = false; private _toDispose = new Set<IDisposable>(); private _isDisposed = false; /** * Dispose of all registered disposables and mark this object as disposed. * * Any future disposables added to this object will be disposed of on `add`. */ public dispose(): void { if (this._isDisposed) { return; } markTracked(this); this._isDisposed = true; this.clear(); } /** * Dispose of all registered disposables but do not mark this object as disposed. */ public clear(): void { try { dispose(this._toDispose.values()); } finally { this._toDispose.clear(); } } public add<T extends IDisposable>(t: T): T { if (!t) { return t; } if ((t as unknown as DisposableStore) === this) { throw new Error('Cannot register a disposable on itself!'); } markTracked(t); if (this._isDisposed) { if (!DisposableStore.DISABLE_DISPOSED_WARNING) { console.warn( new Error( 'Trying to add a disposable to a DisposableStore that has already been disposed of. The added object will be leaked!', ).stack, ); } } else { this._toDispose.add(t); } return t; } } @injectable() export abstract class VSDisposable implements IDisposable { static readonly None = Object.freeze<IDisposable>({ dispose() { // }, }); private readonly _store = new DisposableStore(); constructor() { trackDisposable(this); } public dispose(): void { markTracked(this); this._store.dispose(); } protected _register<T extends IDisposable>(t: T): T { if ((t as unknown as VSDisposable) === this) { throw new Error('Cannot register a disposable on itself!'); } return this._store.add(t); } } /** * Manages the lifecycle of a disposable value that may be changed. * * This ensures that when the disposable value is changed, the previously held disposable is disposed of. You can * also register a `MutableDisposable` on a `VSDisposable` to ensure it is automatically cleaned up. */ export class MutableDisposable<T extends IDisposable> implements IDisposable { private _value?: T | undefined; private _isDisposed = false; constructor() { trackDisposable(this); } get value(): T | undefined { return this._isDisposed ? undefined : this._value; } set value(value: T | undefined) { if (this._isDisposed || value === this._value) { return; } this._value?.dispose(); if (value) { markTracked(value); } this._value = value; } clear() { this.value = undefined; } dispose(): void { this._isDisposed = true; markTracked(this); this._value?.dispose(); this._value = undefined; } } export interface IReference<T> extends IDisposable { readonly object: T; } export abstract class ReferenceCollection<T> { private readonly references: Map<string, { readonly object: T; counter: number }> = new Map(); acquire(key: string, ...args: any[]): IReference<T> { let reference = this.references.get(key); if (!reference) { reference = { counter: 0, object: this.createReferencedObject(key, ...args) }; this.references.set(key, reference); } const { object } = reference; // eslint-disable-next-line @typescript-eslint/no-shadow const dispose = once(() => { if (--reference!.counter === 0) { this.destroyReferencedObject(key, reference!.object); this.references.delete(key); } }); reference.counter++; return { object, dispose }; } protected abstract createReferencedObject(key: string, ...args: any[]): T; protected abstract destroyReferencedObject(key: string, object: T): void; } export class ImmortalReference<T> implements IReference<T> { public object: T; constructor(object: T) { this.object = object; } dispose(): void { /* noop */ } }