Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add comment-documentation for several key Theia utility classes #13324

Merged
merged 5 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 66 additions & 1 deletion packages/core/src/common/disposable.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import { expect } from 'chai';
import { expect, spy, use } from 'chai';
import { DisposableCollection, Disposable } from './disposable';
import * as spies from 'chai-spies';

use(spies);

describe('Disposables', () => {
it('Is safe to use Disposable.NULL', () => {
Expand All @@ -26,4 +29,66 @@ describe('Disposables', () => {
expect(collectionA.disposed, 'A should be disposed after being disposed.').to.be.true;
expect(collectionB.disposed, 'B should not be disposed because A was disposed.').to.be.false;
});

it('Collection is auto-pruned when an element is disposed', () => {
const onDispose = spy(() => { });
const elementDispose = () => { };

const collection = new DisposableCollection();
collection.onDispose(onDispose);

const disposable1 = Disposable.create(elementDispose);
collection.push(disposable1);
expect(collection['disposables']).to.have.lengthOf(1);

const disposable2 = Disposable.create(elementDispose);
collection.push(disposable2);
expect(collection['disposables']).to.have.lengthOf(2);

disposable1.dispose();
expect(collection['disposables']).to.have.lengthOf(1);
expect(onDispose).to.have.not.been.called();
expect(collection.disposed).is.false;

// Test that calling dispose on an already disposed element doesn't
// alter the collection state
disposable1.dispose();
expect(collection['disposables']).to.have.lengthOf(1);
expect(onDispose).to.have.not.been.called();
expect(collection.disposed).is.false;

disposable2.dispose();
expect(collection['disposables']).to.be.empty;
expect(collection.disposed).is.true;
expect(onDispose).to.have.been.called.once;
});

it('onDispose is only called once on actual disposal of elements', () => {
const onDispose = spy(() => { });
const elementDispose = spy(() => { });

const collection = new DisposableCollection();
collection.onDispose(onDispose);

// if the collection is empty 'onDispose' is not called
collection.dispose();
expect(onDispose).to.not.have.been.called();

// 'onDispose' is called because we actually dispose an element
collection.push(Disposable.create(elementDispose));
collection.dispose();
expect(elementDispose).to.have.been.called.once;
expect(onDispose).to.have.been.called.once;

// if the collection is empty 'onDispose' is not called and no further element is disposed
collection.dispose();
expect(elementDispose).to.have.been.called.once;
expect(onDispose).to.have.been.called.once;

// 'onDispose' is not called again even if we actually dispose an element
collection.push(Disposable.create(elementDispose));
collection.dispose();
expect(elementDispose).to.have.been.called.twice;
expect(onDispose).to.have.been.called.once;
});
});
28 changes: 28 additions & 0 deletions packages/core/src/common/disposable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,34 @@ Object.defineProperty(Disposable, 'NULL', {
}
});

/**
* Utility for tracking a collection of Disposable objects.
*
* This utility provides a number of benefits over just using an array of
* Disposables:
*
* - the collection is auto-pruned when an element it contains is disposed by
* any code that has a reference to it
* - you can register to be notified when all elements in the collection have
* been disposed [1]
* - you can conveniently dispose all elements by calling dispose()
* on the collection
*
* Unlike an array, however, this utility does not give you direct access to
* its elements.
*
* Being notified when all elements are disposed is simple:
* ```
* const dc = new DisposableCollection(myDisposables);
* dc.onDispose(() => {
* console.log('All elements in the collection have been disposed');
martin-fleck-at marked this conversation as resolved.
Show resolved Hide resolved
* });
* ```
*
* [1] The collection will notify only once. It will continue to function in so
* far as accepting new Disposables and pruning them when they are disposed, but
* such activity will never result in another notification.
*/
martin-fleck-at marked this conversation as resolved.
Show resolved Hide resolved
export class DisposableCollection implements Disposable {

protected readonly disposables: Disposable[] = [];
Expand Down
55 changes: 55 additions & 0 deletions packages/core/src/common/reference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,50 @@ export interface Reference<T> extends Disposable {
readonly object: T
}

/**
* Abstract class for a map of reference-counted disposable objects, with the
* following features:
*
* - values are not inserted explicitly; instead, acquire() is used to
* create the value for a given key, or return the previously created
* value for it. How the value is created for a given key is
* implementation specific.
*
* - any subsquent acquire() with the same key will bump the reference
* count on that value. acquire() returns not the value directly but
* a reference object that holds the value. Calling dispose() on the
* reference decreases the value's effective reference count.
*
* - a contained value will have its dispose() function called when its
* reference count reaches zero. The key/value pair will be purged
* from the collection.
*
* - calling dispose() on the value directly, instead of calling it on
* the reference returned by acquire(), will automatically dispose
* all outstanding references to that value and the key/value pair
* will be purged from the collection.
*
* - supports synchronous and asynchronous implementations. acquire() will
* return a Promise if the value cannot be created immediately
*
* - functions has|keys|values|get are always synchronous and the result
* excludes asynchronous additions in flight.
*
* - functions values|get return the value directly and not a reference
* to the value. Use these functions to obtain a value without bumping
* its reference count.
*
* - clients can register to be notified when values are added and removed;
* notification for asynchronous additions happen when the creation
* completes, not when it's requested.
*
* - keys can be any value/object that can be successfully stringified using
* JSON.stringify(), sans arguments
*
* - calling dispose() on the collection will dispose all outstanding
* references to all contained values, which results in the disposal of
* the values themselves.
*/
export abstract class AbstractReferenceCollection<K, V extends Disposable> implements Disposable {

protected readonly _keys = new Map<string, K>();
Expand Down Expand Up @@ -108,6 +152,12 @@ export abstract class AbstractReferenceCollection<K, V extends Disposable> imple

}

/**
* Asynchronous implementation of AbstractReferenceCollection that requires
* the client to provide a value factory, used to service the acquire()
* function. That factory may return a Promise if the value cannot be
* created immediately.
*/
export class ReferenceCollection<K, V extends Disposable> extends AbstractReferenceCollection<K, V> {

constructor(protected readonly factory: (key: K) => MaybePromise<V>) {
Expand Down Expand Up @@ -148,6 +198,11 @@ export class ReferenceCollection<K, V extends Disposable> extends AbstractRefere

}

/**
* Synchronous implementation of AbstractReferenceCollection that requires
* the client to provide a value factory, used to service the acquire()
* function.
*/
export class SyncReferenceCollection<K, V extends Disposable> extends AbstractReferenceCollection<K, V> {

constructor(protected readonly factory: (key: K) => V) {
Expand Down
Loading