-
Notifications
You must be signed in to change notification settings - Fork 59
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(util): managed scope and util for handling freeable objects
- Loading branch information
Showing
4 changed files
with
128 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
}); |