@pkerschbaum/code-oss-file-service
Version:
VS Code ([microsoft/vscode](https://github.com/microsoft/vscode)) includes a rich "`FileService`" and "`DiskFileSystemProvider`" abstraction built on top of Node.js core modules (`fs`, `path`) and Electron's `shell` module. This package allows to use that
413 lines (340 loc) • 10.7 kB
text/typescript
/*---------------------------------------------------------------------------------------------
* 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 'vs/base/common/functional';
import { Iterable } from 'vs/base/common/iterator';
/**
* 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: IDisposableTracker | null = null;
export interface IDisposableTracker {
/**
* Is called on construction of a disposable.
*/
trackDisposable(disposable: IDisposable): void;
/**
* Is called when a disposable is registered as child of another disposable (e.g. {@link DisposableStore}).
* If parent is `null`, the disposable is removed from its former parent.
*/
setParent(child: IDisposable, parent: IDisposable | null): void;
/**
* Is called after a disposable is disposed.
*/
markAsDisposed(disposable: IDisposable): void;
/**
* Indicates that the given object is a singleton which does not need to be disposed.
*/
markAsSingleton(disposable: IDisposable): void;
}
export function setDisposableTracker(tracker: IDisposableTracker | null): void {
disposableTracker = tracker;
}
if (TRACK_DISPOSABLES) {
const __is_disposable_tracked__ = '__is_disposable_tracked__';
setDisposableTracker(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.log(stack);
}
}, 3000);
}
setParent(child: IDisposable, parent: IDisposable | null): void {
if (child && child !== Disposable.None) {
try {
(child as any)[__is_disposable_tracked__] = true;
} catch {
// noop
}
}
}
markAsDisposed(disposable: IDisposable): void {
if (disposable && disposable !== Disposable.None) {
try {
(disposable as any)[__is_disposable_tracked__] = true;
} catch {
// noop
}
}
}
markAsSingleton(disposable: IDisposable): void { }
});
}
function trackDisposable<T extends IDisposable>(x: T): T {
disposableTracker?.trackDisposable(x);
return x;
}
function markAsDisposed(disposable: IDisposable): void {
disposableTracker?.markAsDisposed(disposable);
}
function setParentOfDisposable(child: IDisposable, parent: IDisposable | null): void {
disposableTracker?.setParent(child, parent);
}
function setParentOfDisposables(children: IDisposable[], parent: IDisposable | null): void {
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<T extends IDisposable>(singleton: T): T {
disposableTracker?.markAsSingleton(singleton);
return singleton;
}
export class MultiDisposeError extends Error {
constructor(
public readonly errors: any[]
) {
super(`Encountered errors while disposing of store. Errors: [${errors.join(', ')}]`);
}
}
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: Array<T>): Array<T>;
export function dispose<T extends IDisposable>(disposables: ReadonlyArray<T>): ReadonlyArray<T>;
export function dispose<T extends IDisposable>(arg: T | IterableIterator<T> | undefined): any {
if (Iterable.is(arg)) {
let errors: any[] = [];
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 MultiDisposeError(errors);
}
return Array.isArray(arg) ? [] : arg;
} else if (arg) {
arg.dispose();
return arg;
}
}
export function combinedDisposable(...disposables: IDisposable[]): IDisposable {
const parent = toDisposable(() => dispose(disposables));
setParentOfDisposables(disposables, parent);
return parent;
}
export function toDisposable(fn: () => void): IDisposable {
const self = trackDisposable({
dispose: once(() => {
markAsDisposed(self);
fn();
})
});
return self;
}
export class DisposableStore implements IDisposable {
static DISABLE_DISPOSED_WARNING = false;
private _toDispose = new Set<IDisposable>();
private _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`.
*/
public dispose(): void {
if (this._isDisposed) {
return;
}
markAsDisposed(this);
this._isDisposed = true;
this.clear();
}
/**
* Returns `true` if this object has been disposed
*/
public get isDisposed(): boolean {
return this._isDisposed;
}
/**
* 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>(o: T): T {
if (!o) {
return o;
}
if ((o as unknown as DisposableStore) === 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;
}
}
export abstract class Disposable implements IDisposable {
static readonly None = Object.freeze<IDisposable>({ dispose() { } });
private readonly _store = new DisposableStore();
constructor() {
trackDisposable(this);
setParentOfDisposable(this._store, this);
}
public dispose(): void {
markAsDisposed(this);
this._store.dispose();
}
protected _register<T extends IDisposable>(o: T): T {
if ((o as unknown as Disposable) === 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<T extends IDisposable> implements IDisposable {
private _value?: T;
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) {
setParentOfDisposable(value, this);
}
this._value = value;
}
clear() {
this.value = undefined;
}
dispose(): void {
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(): T | undefined {
const oldValue = this._value;
this._value = undefined;
if (oldValue) {
setParentOfDisposable(oldValue, null);
}
return oldValue;
}
}
export class RefCountedDisposable {
private _counter: number = 1;
constructor(
private readonly _disposable: IDisposable,
) { }
acquire() {
this._counter++;
return this;
}
release() {
if (--this._counter === 0) {
this._disposable.dispose();
}
return this;
}
}
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;
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;
}
/**
* Unwraps a reference collection of promised values. Makes sure
* references are disposed whenever promises get rejected.
*/
export class AsyncReferenceCollection<T> {
constructor(private referenceCollection: ReferenceCollection<Promise<T>>) { }
async acquire(key: string, ...args: any[]): Promise<IReference<T>> {
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<T> implements IReference<T> {
constructor(public object: T) { }
dispose(): void { /* noop */ }
}
export function disposeOnReturn(fn: (store: DisposableStore) => void): void {
const store = new DisposableStore();
try {
fn(store);
} finally {
store.dispose();
}
}