Skip to content

Commit

Permalink
Add common Disposable API
Browse files Browse the repository at this point in the history
Provide a reusable Disposable API similar to what Theia/VS Code uses
Fixes eclipse-glsp/glsp#867
  • Loading branch information
tortmayr committed Jan 18, 2023
1 parent f51ddf9 commit 7365722
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 8 deletions.
3 changes: 2 additions & 1 deletion packages/protocol/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/********************************************************************************
* Copyright (c) 2020-2022 EclipseSource and others.
* Copyright (c) 2020-2023 EclipseSource and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
Expand Down Expand Up @@ -53,5 +53,6 @@ export * from './jsonrpc/glsp-jsonrpc-client';
export * from './model/default-types';
export * from './model/model-schema';
export * from './utils/array-util';
export * from './utils/disposable';
export * from './utils/type-util';
export { SetBoundsAction, SetViewportAction };
4 changes: 2 additions & 2 deletions packages/protocol/src/jsonrpc/glsp-jsonrpc-client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/********************************************************************************
* Copyright (c) 2020-2022 EclipseSource and others.
* Copyright (c) 2020-2023 EclipseSource and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
Expand All @@ -22,8 +22,8 @@ import {
InitializeParameters,
InitializeResult
} from '../glsp-client';
import { MaybePromise } from '../utils/type-util';

export type MaybePromise<T> = T | Promise<T> | PromiseLike<T>;
export type ConnectionProvider = MessageConnection | (() => MaybePromise<MessageConnection>);

export namespace JsonrpcGLSPClient {
Expand Down
67 changes: 67 additions & 0 deletions packages/protocol/src/utils/disposable.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/********************************************************************************
* Copyright (c) 2023 STMicroelectronics and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { expect } from 'chai';
import * as sinon from 'sinon';
import { Disposable, DisposableCollection } from './disposable';

describe('Disposable', () => {
describe('DisposableCollection', () => {
let disposableCollection: DisposableCollection;
beforeEach(() => (disposableCollection = new DisposableCollection()));
describe('push', () => {
it('should add one disposable to the collection and remove it again', () => {
const disposable = Disposable.empty();
const toRemove = disposableCollection.push(disposable);
expect(disposableCollection['disposables'].length).to.be.equal(1);
expect(disposableCollection['disposables'][0]).to.equal(disposable);
toRemove.dispose();
expect(disposableCollection['disposables'].length).to.be.equal(0);
});
it('should add multiple disposable to the collection and remove them again', () => {
const disposable1 = Disposable.empty();
const disposable2 = Disposable.empty();
const disposable3 = Disposable.empty();

const toRemove = disposableCollection.push(disposable1, disposable2, disposable3);
expect(disposableCollection['disposables'].length).to.be.equal(3);
expect(disposableCollection['disposables'][0]).to.equal(disposable1);
expect(disposableCollection['disposables'][1]).to.equal(disposable2);
expect(disposableCollection['disposables'][2]).to.equal(disposable3);
toRemove.dispose();
expect(disposableCollection['disposables'].length).to.be.equal(0);
});
});
describe('dispose', () => {
describe('should invoke dispose on all elements of the collection exactly once', () => {
const disposable1 = Disposable.empty();
const disposable2 = Disposable.empty();
const disposable1Spy = sinon.spy(disposable1);
const disposable2Spy = sinon.spy(disposable2);

disposableCollection = new DisposableCollection(disposable1, disposable2);

disposableCollection.dispose();
disposableCollection.dispose();
disposableCollection.dispose();

expect(disposable1Spy.dispose.calledOnce).to.be.true;
expect(disposable2Spy.dispose.calledOnce).to.be.true;
expect(disposableCollection['disposables'].length).to.be.equal(0);
});
});
});
});
95 changes: 95 additions & 0 deletions packages/protocol/src/utils/disposable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/********************************************************************************
* Copyright (c) 2022-2023 STMicroelectronics and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import { remove } from './array-util';
import { AnyObject, hasFunctionProp } from './type-util';

/**
* Interface for objects that can or need to be disposed properly.
*/
export interface Disposable {
/**
* Dispose this object.
*/
dispose(): void;
}

export namespace Disposable {
export function is(value: unknown): value is Disposable {
return AnyObject.is(value) && hasFunctionProp(value, '');
}

/**
* Creates a new empty i.e. no-op {@link Disposable}.
* @returns the newly created disposable
*/
export function empty(): Disposable {
// eslint-disable-next-line @typescript-eslint/no-empty-function
return { dispose: () => {} };
}

/**
* Creates a new {@link Disposable} that delegates to the given callback.
* @param cb The callback that should be invoked on dispose
* @returns the newly created disposable
*/
export function create(cb: () => void): Disposable {
return { dispose: cb };
}
}

/**
* Reusable base class to manage a collection of {@link Disposable}s.
*/
export class DisposableCollection implements Disposable {
protected readonly disposables: Disposable[] = [];
errorHandler?: (err: any) => void;

constructor(...toDispose: Disposable[]) {
toDispose.forEach(d => this.push(d));
this.errorHandler = err => console.error(err);
}

dispose(): void {
if (this.disposed) {
return;
}
try {
while (!this.disposed) {
this.disposables.pop()?.dispose();
}
} catch (err) {
this.errorHandler?.(err);
}
}

get disposed(): boolean {
return this.disposables.length === 0;
}

/**
* Pushes the given disposables to the collection.
* @param disposables The disposables that should be added
* @returns A disposable that removes the previously pushed values from the collection when invoked
*/
push(...disposables: Disposable[]): Disposable {
this.disposables.push(...disposables);
return Disposable.create(() => remove(this.disposables, ...disposables));
}

get isDisposed(): boolean {
return this.disposed;
}
}
17 changes: 15 additions & 2 deletions packages/protocol/src/utils/type-util.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/********************************************************************************
* Copyright (c) 2022 STMicroelectronics and others.
* Copyright (c) 2022-2023 STMicroelectronics and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
Expand All @@ -14,7 +14,7 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import { expect } from 'chai';
import { AnyObject, hasArrayProp, hasBooleanProp, hasNumberProp, hasObjectProp, hasStringProp } from './type-util';
import { AnyObject, hasArrayProp, hasBooleanProp, hasFunctionProp, hasNumberProp, hasObjectProp, hasStringProp } from './type-util';

describe('TypeUtil', () => {
describe('AnyObject', () => {
Expand Down Expand Up @@ -95,6 +95,19 @@ describe('TypeUtil', () => {
});
});

describe('hasFunctionProp', () => {
it('should return true for an object that has a property that matches the given key and type', () => {
// eslint-disable-next-line @typescript-eslint/no-empty-function
expect(hasFunctionProp({ someProp: () => {} }, 'someProp')).to.be.true;
});
it('should return false for an object that has a property that matches the given but not the given type', () => {
expect(hasFunctionProp({ someProp: '123' }, 'someProp')).to.be.false;
});
it('should return false for an object that does not have a property that matches the given key.', () => {
expect(hasFunctionProp({ anotherProp: 123 }, 'someProp')).to.be.false;
});
});

describe('hasArrayProp', () => {
it('should return true for an object that has a property that matches the given key and type', () => {
expect(hasArrayProp({ someProp: ['some', 'prop'] }, 'someProp')).to.be.true;
Expand Down
21 changes: 18 additions & 3 deletions packages/protocol/src/utils/type-util.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/********************************************************************************
* Copyright (c) 2021-2022 EclipseSource and others.
* Copyright (c) 2021-2023 EclipseSource and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
Expand Down Expand Up @@ -53,10 +53,15 @@ export interface Constructor<T> {
// eslint-disable-next-line @typescript-eslint/ban-types
export type Writable<T> = { -readonly [P in keyof T]: Writable<T[P]> };

/**
* Utility type to describe a value as might be provided as a promise.
*/
export type MaybePromise<T> = T | PromiseLike<T>;

/**
* Utility type to describe typeguard functions.
*/
export type TypeGuard<T> = (element: any, ...args: any[]) => element is T;
export type TypeGuard<T> = (element: any) => element is T;

/**
* Utility function that create a typeguard function for a given class constructor.
Expand All @@ -65,7 +70,7 @@ export type TypeGuard<T> = (element: any, ...args: any[]) => element is T;
* @returns The typeguard for this class.
*/
export function toTypeGuard<G>(constructor: Constructor<G>): TypeGuard<G> {
return (element: any): element is G => element instanceof constructor;
return (element: unknown): element is G => element instanceof constructor;
}

/**
Expand Down Expand Up @@ -108,6 +113,16 @@ export function hasObjectProp(object: AnyObject, propertyKey: string): boolean {
return propertyKey in object && AnyObject.is(object[propertyKey]);
}

/**
* Validates whether the given object as a property of type `function` with the given key.
* @param object The object that should be validated
* @param propertyKey The key of the property
* @returns `true` if the object has property with matching key of type `function`.
*/
export function hasFunctionProp(object: AnyObject, propertyKey: string): boolean {
return propertyKey in object && typeof object[propertyKey] === 'function';
}

/**
* Validates whether the given object as a property of type `Array` with the given key.
* @param object The object that should be validated
Expand Down

0 comments on commit 7365722

Please sign in to comment.