Skip to content

Commit 7f03a53

Browse files
committed
feat(util): managed scope and util for handling freeable objects
1 parent bb3f3d1 commit 7f03a53

File tree

4 files changed

+128
-0
lines changed

4 files changed

+128
-0
lines changed

Diff for: packages/util/src/freeable.ts

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { Freeable } from './types';
2+
3+
/**
4+
* A scope to ease the management of objects that require manual resource management.
5+
*
6+
*/
7+
export class ManagedFreeableScope {
8+
#scopeStack: Freeable[] = [];
9+
#disposed = false;
10+
11+
/**
12+
* Objects passed to this method will then be managed by the instance.
13+
*
14+
* @param freeable An object with a free function, or undefined. This makes it suitable for wrapping functions that
15+
* may or may not return a value, to minimise the implementation logic.
16+
* @returns The freeable object passed in, which can be undefined.
17+
*/
18+
public manage<T extends Freeable | undefined>(freeable: T): T {
19+
if (freeable === undefined) return freeable;
20+
if (this.#disposed) throw new Error('This scope is already disposed.');
21+
this.#scopeStack.push(freeable);
22+
return freeable;
23+
}
24+
25+
/**
26+
* Once the freeable objects being managed are no longer being accessed, call this method.
27+
*/
28+
public dispose(): void {
29+
if (this.#disposed) return;
30+
for (const resource of this.#scopeStack) {
31+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
32+
if ((resource as any)?.ptr === 0 || !resource?.free) {
33+
continue;
34+
}
35+
36+
resource?.free();
37+
}
38+
this.#disposed = true;
39+
}
40+
}
41+
42+
class AutoFree<TReturn> {
43+
#scope: ManagedFreeableScope;
44+
readonly #callback: (scope: ManagedFreeableScope) => TReturn;
45+
46+
constructor(cb: (scope: ManagedFreeableScope) => TReturn) {
47+
this.#callback = cb;
48+
this.#scope = new ManagedFreeableScope();
49+
}
50+
51+
public execute() {
52+
try {
53+
return this.#callback(this.#scope);
54+
} finally {
55+
this.#scope.dispose();
56+
}
57+
}
58+
}
59+
60+
/**
61+
* A wrapper function to setup and dispose of a ManagedFreeableScope at the end of the callback execution.
62+
*/
63+
export const usingAutoFree = <TReturn>(cb: (scope: ManagedFreeableScope) => TReturn) =>
64+
new AutoFree<TReturn>(cb).execute();

Diff for: packages/util/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './equals';
2+
export * from './freeable';
23
export * from './types';
34
export * from './BigIntMath';
45
export * from './hexString';

Diff for: packages/util/src/types.ts

+4
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,7 @@ export type DeepPartial<T, O = never> = T extends O | Primitive
1515
: {
1616
[P in keyof T]?: DeepPartial<T[P], O>;
1717
};
18+
19+
export interface Freeable {
20+
free: () => void;
21+
}

Diff for: packages/util/test/freeable.test.ts

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { Freeable, ManagedFreeableScope, usingAutoFree } from '../src';
2+
3+
class FreeableEntity implements Freeable {
4+
constructor(public id: number) {}
5+
public getId(): number {
6+
return this.id;
7+
}
8+
public free() {
9+
void 0;
10+
}
11+
}
12+
13+
describe('freeable', () => {
14+
describe('ManagedFreeableScope', () => {
15+
it('manage returns undefined if argument passed is undefined', () => {
16+
const scope = new ManagedFreeableScope();
17+
const freeable = undefined;
18+
const one = scope.manage(freeable);
19+
expect(one).toBeUndefined();
20+
});
21+
});
22+
describe('usingAutoFree', () => {
23+
it('calls the object free method after executing callback', () => {
24+
const entity = new FreeableEntity(1);
25+
const spy = jest.spyOn(entity, 'free');
26+
usingAutoFree((scope) => {
27+
const one = scope.manage(entity);
28+
expect(one.getId()).toBe(1);
29+
});
30+
expect(spy).toHaveBeenCalledTimes(1);
31+
});
32+
33+
it('can return a value', () => {
34+
const entity = new FreeableEntity(1);
35+
const spy = jest.spyOn(entity, 'free');
36+
const id = usingAutoFree((scope) => {
37+
const one = scope.manage(entity);
38+
return one.getId();
39+
});
40+
expect(id).toBe(1);
41+
expect(spy).toHaveBeenCalledTimes(1);
42+
});
43+
44+
it('can handle multiple objects', () => {
45+
const firstEntity = new FreeableEntity(1);
46+
const secondEntity = new FreeableEntity(2);
47+
const firstSpy = jest.spyOn(firstEntity, 'free');
48+
const secondSpy = jest.spyOn(secondEntity, 'free');
49+
usingAutoFree((scope) => {
50+
const one = scope.manage(firstEntity);
51+
const two = scope.manage(secondEntity);
52+
expect(one.getId()).toBe(1);
53+
expect(two.getId()).toBe(2);
54+
});
55+
expect(firstSpy).toHaveBeenCalledTimes(1);
56+
expect(secondSpy).toHaveBeenCalledTimes(1);
57+
});
58+
});
59+
});

0 commit comments

Comments
 (0)