Skip to content

Commit

Permalink
feat(util): managed scope and util for handling freeable objects
Browse files Browse the repository at this point in the history
  • Loading branch information
rhyslbw committed Oct 21, 2022
1 parent bb3f3d1 commit 7f03a53
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 0 deletions.
64 changes: 64 additions & 0 deletions packages/util/src/freeable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Freeable } from './types';

/**
* A scope to ease the management of objects that require manual resource management.
*
*/
export class ManagedFreeableScope {
#scopeStack: Freeable[] = [];
#disposed = false;

/**
* Objects passed to this method will then be managed by the instance.
*
* @param freeable An object with a free function, or undefined. This makes it suitable for wrapping functions that
* may or may not return a value, to minimise the implementation logic.
* @returns The freeable object passed in, which can be undefined.
*/
public manage<T extends Freeable | undefined>(freeable: T): T {
if (freeable === undefined) return freeable;
if (this.#disposed) throw new Error('This scope is already disposed.');
this.#scopeStack.push(freeable);
return freeable;
}

/**
* Once the freeable objects being managed are no longer being accessed, call this method.
*/
public dispose(): void {
if (this.#disposed) return;
for (const resource of this.#scopeStack) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((resource as any)?.ptr === 0 || !resource?.free) {
continue;
}

resource?.free();
}
this.#disposed = true;
}
}

class AutoFree<TReturn> {
#scope: ManagedFreeableScope;
readonly #callback: (scope: ManagedFreeableScope) => TReturn;

constructor(cb: (scope: ManagedFreeableScope) => TReturn) {
this.#callback = cb;
this.#scope = new ManagedFreeableScope();
}

public execute() {
try {
return this.#callback(this.#scope);
} finally {
this.#scope.dispose();
}
}
}

/**
* A wrapper function to setup and dispose of a ManagedFreeableScope at the end of the callback execution.
*/
export const usingAutoFree = <TReturn>(cb: (scope: ManagedFreeableScope) => TReturn) =>
new AutoFree<TReturn>(cb).execute();
1 change: 1 addition & 0 deletions packages/util/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './equals';
export * from './freeable';
export * from './types';
export * from './BigIntMath';
export * from './hexString';
Expand Down
4 changes: 4 additions & 0 deletions packages/util/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ export type DeepPartial<T, O = never> = T extends O | Primitive
: {
[P in keyof T]?: DeepPartial<T[P], O>;
};

export interface Freeable {
free: () => void;
}
59 changes: 59 additions & 0 deletions packages/util/test/freeable.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Freeable, ManagedFreeableScope, usingAutoFree } from '../src';

class FreeableEntity implements Freeable {
constructor(public id: number) {}
public getId(): number {
return this.id;
}
public free() {
void 0;
}
}

describe('freeable', () => {
describe('ManagedFreeableScope', () => {
it('manage returns undefined if argument passed is undefined', () => {
const scope = new ManagedFreeableScope();
const freeable = undefined;
const one = scope.manage(freeable);
expect(one).toBeUndefined();
});
});
describe('usingAutoFree', () => {
it('calls the object free method after executing callback', () => {
const entity = new FreeableEntity(1);
const spy = jest.spyOn(entity, 'free');
usingAutoFree((scope) => {
const one = scope.manage(entity);
expect(one.getId()).toBe(1);
});
expect(spy).toHaveBeenCalledTimes(1);
});

it('can return a value', () => {
const entity = new FreeableEntity(1);
const spy = jest.spyOn(entity, 'free');
const id = usingAutoFree((scope) => {
const one = scope.manage(entity);
return one.getId();
});
expect(id).toBe(1);
expect(spy).toHaveBeenCalledTimes(1);
});

it('can handle multiple objects', () => {
const firstEntity = new FreeableEntity(1);
const secondEntity = new FreeableEntity(2);
const firstSpy = jest.spyOn(firstEntity, 'free');
const secondSpy = jest.spyOn(secondEntity, 'free');
usingAutoFree((scope) => {
const one = scope.manage(firstEntity);
const two = scope.manage(secondEntity);
expect(one.getId()).toBe(1);
expect(two.getId()).toBe(2);
});
expect(firstSpy).toHaveBeenCalledTimes(1);
expect(secondSpy).toHaveBeenCalledTimes(1);
});
});
});

0 comments on commit 7f03a53

Please sign in to comment.