UNPKG

@sussudio/base

Version:

Internal APIs for VS Code's utilities and user interface building blocks.

449 lines (448 loc) 10.8 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { once } from './functional.mjs'; import { Iterable } from './iterator.mjs'; // #region Disposable Tracking /** * 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 Disposable or use a DisposableStore. This means there are a lot of false positives. */ const TRACK_DISPOSABLES = false; let disposableTracker = null; export function setDisposableTracker(tracker) { disposableTracker = tracker; } if (TRACK_DISPOSABLES) { const __is_disposable_tracked__ = '__is_disposable_tracked__'; setDisposableTracker( new (class { trackDisposable(x) { const stack = new Error('Potentially leaked disposable').stack; setTimeout(() => { if (!x[__is_disposable_tracked__]) { console.log(stack); } }, 3000); } setParent(child, parent) { if (child && child !== Disposable.None) { try { child[__is_disposable_tracked__] = true; } catch { // noop } } } markAsDisposed(disposable) { if (disposable && disposable !== Disposable.None) { try { disposable[__is_disposable_tracked__] = true; } catch { // noop } } } markAsSingleton(disposable) {} })(), ); } function trackDisposable(x) { disposableTracker?.trackDisposable(x); return x; } function markAsDisposed(disposable) { disposableTracker?.markAsDisposed(disposable); } function setParentOfDisposable(child, parent) { disposableTracker?.setParent(child, parent); } function setParentOfDisposables(children, parent) { if (!disposableTracker) { return; } for (const child of children) { disposableTracker.setParent(child, parent); } } /** * Indicates that the given object is a singleton which does not need to be disposed. */ export function markAsSingleton(singleton) { disposableTracker?.markAsSingleton(singleton); return singleton; } /** * Check if `thing` is {@link IDisposable disposable}. */ export function isDisposable(thing) { return typeof thing.dispose === 'function' && thing.dispose.length === 0; } export function dispose(arg) { if (Iterable.is(arg)) { const errors = []; for (const d of arg) { if (d) { try { d.dispose(); } catch (e) { errors.push(e); } } } if (errors.length === 1) { throw errors[0]; } else if (errors.length > 1) { throw new AggregateError(errors, 'Encountered errors while disposing of store'); } return Array.isArray(arg) ? [] : arg; } else if (arg) { arg.dispose(); return arg; } } export function disposeIfDisposable(disposables) { for (const d of disposables) { if (isDisposable(d)) { d.dispose(); } } return []; } /** * Combine multiple disposable values into a single {@link IDisposable}. */ export function combinedDisposable(...disposables) { const parent = toDisposable(() => dispose(disposables)); setParentOfDisposables(disposables, parent); return parent; } /** * Turn a function that implements dispose into an {@link IDisposable}. */ export function toDisposable(fn) { const self = trackDisposable({ dispose: once(() => { markAsDisposed(self); fn(); }), }); return self; } /** * Manages a collection of disposable values. * * This is the preferred way to manage multiple disposables. A `DisposableStore` is safer to work with than an * `IDisposable[]` as it considers edge cases, such as registering the same value multiple times or adding an item to a * store that has already been disposed of. */ export class DisposableStore { static DISABLE_DISPOSED_WARNING = false; _toDispose = new Set(); _isDisposed = false; constructor() { trackDisposable(this); } /** * Dispose of all registered disposables and mark this object as disposed. * * Any future disposables added to this object will be disposed of on `add`. */ dispose() { if (this._isDisposed) { return; } markAsDisposed(this); this._isDisposed = true; this.clear(); } /** * @return `true` if this object has been disposed of. */ get isDisposed() { return this._isDisposed; } /** * Dispose of all registered disposables but do not mark this object as disposed. */ clear() { if (this._toDispose.size === 0) { return; } try { dispose(this._toDispose); } finally { this._toDispose.clear(); } } /** * Add a new {@link IDisposable disposable} to the collection. */ add(o) { if (!o) { return o; } if (o === this) { throw new Error('Cannot register a disposable on itself!'); } setParentOfDisposable(o, this); 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(o); } return o; } } /** * Abstract base class for a {@link IDisposable disposable} object. * * Subclasses can {@linkcode _register} disposables that will be automatically cleaned up when this object is disposed of. */ export class Disposable { /** * A disposable that does nothing when it is disposed of. * * TODO: This should not be a static property. */ static None = Object.freeze({ dispose() {} }); _store = new DisposableStore(); constructor() { trackDisposable(this); setParentOfDisposable(this._store, this); } dispose() { markAsDisposed(this); this._store.dispose(); } /** * Adds `o` to the collection of disposables managed by this object. */ _register(o) { if (o === this) { throw new Error('Cannot register a disposable on itself!'); } return this._store.add(o); } } /** * 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 `Disposable` to ensure it is automatically cleaned up. */ export class MutableDisposable { _value; _isDisposed = false; constructor() { trackDisposable(this); } get value() { return this._isDisposed ? undefined : this._value; } set value(value) { if (this._isDisposed || value === this._value) { return; } this._value?.dispose(); if (value) { setParentOfDisposable(value, this); } this._value = value; } /** * Resets the stored value and disposed of the previously stored value. */ clear() { this.value = undefined; } dispose() { this._isDisposed = true; markAsDisposed(this); this._value?.dispose(); this._value = undefined; } /** * Clears the value, but does not dispose it. * The old value is returned. */ clearAndLeak() { const oldValue = this._value; this._value = undefined; if (oldValue) { setParentOfDisposable(oldValue, null); } return oldValue; } } export class RefCountedDisposable { _disposable; _counter = 1; constructor(_disposable) { this._disposable = _disposable; } acquire() { this._counter++; return this; } release() { if (--this._counter === 0) { this._disposable.dispose(); } return this; } } /** * A safe disposable can be `unset` so that a leaked reference (listener) * can be cut-off. */ export class SafeDisposable { dispose = () => {}; unset = () => {}; isset = () => false; constructor() { trackDisposable(this); } set(fn) { let callback = fn; this.unset = () => (callback = undefined); this.isset = () => callback !== undefined; this.dispose = () => { if (callback) { callback(); callback = undefined; markAsDisposed(this); } }; return this; } } export class ReferenceCollection { references = new Map(); acquire(key, ...args) { let reference = this.references.get(key); if (!reference) { reference = { counter: 0, object: this.createReferencedObject(key, ...args) }; this.references.set(key, reference); } const { object } = reference; const dispose = once(() => { if (--reference.counter === 0) { this.destroyReferencedObject(key, reference.object); this.references.delete(key); } }); reference.counter++; return { object, dispose }; } } /** * Unwraps a reference collection of promised values. Makes sure * references are disposed whenever promises get rejected. */ export class AsyncReferenceCollection { referenceCollection; constructor(referenceCollection) { this.referenceCollection = referenceCollection; } async acquire(key, ...args) { const ref = this.referenceCollection.acquire(key, ...args); try { const object = await ref.object; return { object, dispose: () => ref.dispose(), }; } catch (error) { ref.dispose(); throw error; } } } export class ImmortalReference { object; constructor(object) { this.object = object; } dispose() {} } export function disposeOnReturn(fn) { const store = new DisposableStore(); try { fn(store); } finally { store.dispose(); } } /** * A map the manages the lifecycle of the values that it stores. */ export class DisposableMap { _store = new Map(); _isDisposed = false; constructor() { trackDisposable(this); } /** * Disposes of all stored values and mark this object as disposed. * * Trying to use this object after it has been disposed of is an error. */ dispose() { markAsDisposed(this); this._isDisposed = true; this.clearAndDisposeAll(); } /** * Disposes of all stored values and clear the map, but DO NOT mark this object as disposed. */ clearAndDisposeAll() { if (!this._store.size) { return; } try { dispose(this._store.values()); } finally { this._store.clear(); } } has(key) { return this._store.has(key); } get(key) { return this._store.get(key); } set(key, value, skipDisposeOnOverwrite = false) { if (this._isDisposed) { console.warn( new Error( 'Trying to add a disposable to a DisposableMap that has already been disposed of. The added object will be leaked!', ).stack, ); } if (!skipDisposeOnOverwrite) { this._store.get(key)?.dispose(); } this._store.set(key, value); } /** * Delete the value stored for `key` from this map and also dispose of it. */ deleteAndDispose(key) { this._store.get(key)?.dispose(); this._store.delete(key); } [Symbol.iterator]() { return this._store[Symbol.iterator](); } }