Skip to content

Commit

Permalink
clean up disposables
Browse files Browse the repository at this point in the history
  • Loading branch information
Jason3S committed Oct 4, 2023
1 parent 18271b8 commit 0577b78
Show file tree
Hide file tree
Showing 5 changed files with 436 additions and 38 deletions.
79 changes: 79 additions & 0 deletions packages/utils-disposables/src/DisposableList.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { describe, expect, test } from '@jest/globals';

import { type DisposableLike, disposeOf, isDisposableHybrid, isDisposed } from './disposable.js';
import { createDisposableList, DisposableList, InheritableDisposable } from './DisposableList.js';

describe('disposable', () => {
test('InheritableDisposable', () => {
let count = 0;
class MyDisposable extends InheritableDisposable {
constructor(disposables: DisposableLike[]) {
super(disposables);
}
}

function use() {
using _d = new MyDisposable([() => (count += 10)]);
}

expect(count).toBe(0);

use();

expect(count).toBe(10);
});

test('DisposableList', () => {
let count = 0;

function use() {
using list = new DisposableList([() => (count += 10)]);
list.push(() => (count += 100));
}

expect(count).toBe(0);

use();

expect(count).toBe(110);
});

test('createDisposableList', () => {
const list = createDisposableList();
let count = 0;
list.push(() => (count += 1));
disposeOf(list);
expect(count).toBe(1);
expect(isDisposed(list)).toBe(true);
expect(list.isDisposed()).toBe(true);
});

test('double dispose', () => {
let count = 0;
const list = createDisposableList([() => (count += 10)]);
function use() {
using aliasList = list;
aliasList.push(() => (count += 100));
}

expect(list.length).toBe(1);
use();

expect(count).toBe(110);
expect(list.length).toBe(0);

expect(() => list.push(() => (count += 1000))).toThrowError('Already disposed, cannot add items.');

expect(list.length).toBe(0);
list.dispose();
expect(list.length).toBe(0);

expect(count).toBe(110);
});

test('', () => {
const list = createDisposableList();
expect(isDisposableHybrid(list)).toBe(true);
expect(list.isDisposed());
});
});
60 changes: 60 additions & 0 deletions packages/utils-disposables/src/DisposableList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { DisposableHybrid, DisposableLike } from './disposable.js';
import { createDisposeMethodFromList, symbolIsDisposed } from './disposable.js';

/** This is a class that can be inherited to provide Disposable support. */

export const noop = () => undefined;

export class InheritableDisposable implements DisposableHybrid {
public dispose: () => void;
public [Symbol.dispose]: () => void = noop;
public [symbolIsDisposed]: boolean = false;

/** the inherited class can safely add disposables to _disposables */
protected readonly _disposables: DisposableLike[];
constructor(disposables?: DisposableLike[], name = 'InheritableDisposable') {
this._disposables = disposables ?? [];
const _dispose = createDisposeMethodFromList(this._disposables, name);
const dispose = () => {
if (this.isDisposed()) return;
this[symbolIsDisposed] = true;
_dispose();
// Prevent new disposables from being added.
Object.freeze(this._disposables);
};
this.dispose = dispose;
this[Symbol.dispose] = dispose;
}

protected isDisposed(): boolean {
return this[symbolIsDisposed];
}
}

export class DisposableList extends InheritableDisposable {
constructor(
public readonly disposables: DisposableLike[] = [],
readonly name = 'DisposableList',
) {
super(disposables);
}

public push(disposable: DisposableLike) {
if (this.isDisposed()) {
throw new Error('Already disposed, cannot add items.');
}
this.disposables.push(disposable);
}

get length() {
return this.disposables.length;
}

public isDisposed(): boolean {
return super.isDisposed();
}
}

export function createDisposableList(disposables?: DisposableLike[], name?: string): DisposableList {
return new DisposableList(disposables, name);
}
140 changes: 138 additions & 2 deletions packages/utils-disposables/src/disposable.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
import { describe, expect, jest, test } from '@jest/globals';

import type { DisposableLike } from './disposable.js';
import { createDisposable, createDisposableFromList, InheritableDisposable, injectDisposable } from './disposable.js';
import type { DisposableLike, DisposeFn } from './disposable.js';
import {
createDisposable,
createDisposableFromList,
createDisposeMethodFromList,
disposeOf,
getDisposableName,
injectDisposable,
isDisposableHybrid,
isDisposed,
makeDisposable,
setDebugMode,
setDisposableName,
} from './disposable.js';
import { InheritableDisposable } from './DisposableList.js';

describe('disposable', () => {
test('createDisposable', () => {
Expand All @@ -15,6 +28,20 @@ describe('disposable', () => {
expect(dispose).toHaveBeenCalledTimes(1);
});

test('createDisposable named', () => {
const dispose = jest.fn();
const myDisposable = createDisposable(dispose, undefined, 'MyDisposable');

function use() {
using _obj = myDisposable;
}
expect(isDisposed(myDisposable)).toBe(false);
use();
expect(isDisposed(myDisposable)).toBe(true);
expect(getDisposableName(myDisposable)).toBe('MyDisposable');
expect(dispose).toHaveBeenCalledTimes(1);
});

test('createDisposable thisArg', () => {
const myObj = {
callMe: jest.fn(),
Expand Down Expand Up @@ -108,4 +135,113 @@ describe('disposable', () => {

expect(count).toBe(10);
});

test.each`
value | expected
${undefined} | ${false}
${null} | ${false}
${1} | ${false}
${'hello'} | ${false}
${{}} | ${false}
${makeDisposable(() => undefined)} | ${true}
`('isDisposableHybrid', ({ value, expected }) => {
expect(isDisposableHybrid(value)).toBe(expected);
});

test('makeDisposable', () => {
let count = 0;
const a1 = () => (count += 1);
const d1 = makeDisposable(a1);
expect(isDisposableHybrid(d1)).toBe(true);
expect(isDisposed(d1)).toBe(false);
expect(makeDisposable(d1)).toBe(d1);
expect(count).toBe(0);
expect(getDisposableName(d1)).toBe('makeDisposable');

const a2 = { dispose: () => (count += 10) };
const d2 = makeDisposable(a2);
expect(isDisposableHybrid(d2)).toBe(true);

const a3 = { [Symbol.dispose]: () => (count += 100) };
const d3 = makeDisposable(a3);
expect(isDisposableHybrid(d3)).toBe(true);

const a4 = { [Symbol.dispose]: a1, dispose: a1 };
const d4 = makeDisposable(a4, 'a4');
expect(isDisposableHybrid(d4)).toBe(true);
expect(getDisposableName(d4)).toBe('a4');

d4.dispose();
expect(count).toBe(1);
});

test('get/set name', () => {
const d = createDisposable(() => undefined, undefined, 'name1');
expect(getDisposableName(d)).toBe('name1');
setDisposableName(d, 'name2');
expect(getDisposableName(d)).toBe('name2');
});
});

describe('disposable debug', () => {
beforeEach(() => {
setDebugMode(true);
jest.spyOn(console, 'log').mockImplementation(() => undefined);
jest.spyOn(console, 'error').mockImplementation(() => undefined);
});

afterEach(() => {
setDebugMode(false);
jest.resetAllMocks();
});

test('createDisposableFromList', () => {
let count = 0;
const disposables = [
createDisposable(() => (count += 1)),
createDisposable(() => (count += 10)),
createDisposable(() => (count += 100)),
];
function use() {
using _obj = createDisposableFromList(disposables);
}
use();
expect(count).toBe(111);
});

test('dispose with errors', () => {
const e1 = Error('one');
const e2 = Error('two');

const d = createDisposableFromList([
() => {
throw e1;
},
() => {
throw e2;
},
]);

expect(() => disposeOf(d)).toThrow(e2);
});

test('createDisposableFromList double dispose', () => {
const list: DisposeFn[] = [];
const d = createDisposableFromList(list);
d.dispose();
list.push(() => undefined);
d.dispose();
// It was not disposed.
expect(list.length).toBe(1);
});

test('createDisposableFromList double dispose', () => {
const list: DisposeFn[] = [];
const d = createDisposeMethodFromList(list);
d();
list.push(() => undefined);
d();
// It was disposed.
expect(list.length).toBe(0);
});
});
Loading

0 comments on commit 0577b78

Please sign in to comment.