Skip to content

Commit

Permalink
feat: add DisposableHandle and AsyncDisposableHandle
Browse files Browse the repository at this point in the history
  • Loading branch information
giladgd committed Sep 17, 2024
1 parent 7c072ef commit 3df1087
Show file tree
Hide file tree
Showing 6 changed files with 290 additions and 0 deletions.
86 changes: 86 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,92 @@ disposeAggregator.add(async () => {
disposeAggregator.dispose();
```

### `DisposableHandle`
An object that provides a `.dispose()` method that can called only once.

Calling `.dispose()` will call the provided `onDispose` function only once.
Any subsequent calls to `.dispose()` will do nothing.

```typescript
import {DisposableHandle} from "lifecycle-utils";

function createHandle() {
console.log("allocating resources");

return new DisposableHandle(() => {
console.log("resources disposed");
});
}

const handle = createHandle();
handle.dispose();
```

Using the `using` feature of TypeScript is also supported:
```typescript
import {DisposableHandle} from "lifecycle-utils";

function createHandle() {
console.log("allocating resources");

return new DisposableHandle(() => {
console.log("resources disposed");
});
}

function doWork() {
using handle = createHandle();
}

doWork();
// resources disposed
// the dispose function was called since the scope of the `doWork` function ended
```

### `AsyncDisposableHandle`
An object that provides an async `.dispose()` method that can called only once.

Calling `.dispose()` will call the provided `onDispose` function only once.
Any subsequent calls to `.dispose()` will do nothing.

```typescript
import {AsyncDisposableHandle} from "lifecycle-utils";

function createHandle() {
console.log("allocating resources");

return new AsyncDisposableHandle(async () => {
await new Promise(resolve => setTimeout(resolve, 1000));
console.log("resources disposed");
});
}

const handle = createHandle();
await handle.dispose();
```

Using the `await using` feature of TypeScript is also supported:
```typescript
import {AsyncDisposableHandle} from "lifecycle-utils";

function createHandle() {
console.log("allocating resources");

return new AsyncDisposableHandle(async () => {
await new Promise(resolve => setTimeout(resolve, 1000));
console.log("resources disposed");
});
}

async function doWork() {
await using handle = createHandle();
}

await doWork();
// resources disposed
// the dispose function was called since the scope of the `doWork` function ended
```

### `MultiKeyMap`
`MultiKeyMap` is a utility class that works like a `Map`, but accepts multiple values as the key for each value.

Expand Down
33 changes: 33 additions & 0 deletions src/AsyncDisposableHandle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* An object that provides an async `.dispose()` method that can called only once.
*
* Calling `.dispose()` will call the provided `onDispose` function only once.
* Any subsequent calls to `.dispose()` will do nothing.
*/
export class AsyncDisposableHandle {
/** @internal */ private _onDispose: (() => Promise<void>) | undefined;

public constructor(onDispose: () => Promise<void>) {
this._onDispose = onDispose;

this.dispose = this.dispose.bind(this);
this[Symbol.asyncDispose] = this[Symbol.asyncDispose].bind(this);
}

public get disposed() {
return this._onDispose == null;
}

public async [Symbol.asyncDispose]() {
await this.dispose();
}

public async dispose() {
if (this._onDispose != null) {
const onDispose = this._onDispose;
delete this._onDispose;

await onDispose();
}
}
}
33 changes: 33 additions & 0 deletions src/DisposableHandle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* An object that provides a `.dispose()` method that can called only once.
*
* Calling `.dispose()` will call the provided `onDispose` function only once.
* Any subsequent calls to `.dispose()` will do nothing.
*/
export class DisposableHandle {
/** @internal */ private _onDispose: (() => void) | undefined;

public constructor(onDispose: () => void) {
this._onDispose = onDispose;

this.dispose = this.dispose.bind(this);
this[Symbol.dispose] = this[Symbol.dispose].bind(this);
}

public get disposed() {
return this._onDispose == null;
}

public [Symbol.dispose]() {
this.dispose();
}

public dispose() {
if (this._onDispose != null) {
const onDispose = this._onDispose;
delete this._onDispose;

onDispose();
}
}
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export * from "./LongTimeout.js";
export * from "./State.js";
export * from "./DisposeAggregator.js";
export * from "./AsyncDisposeAggregator.js";
export * from "./DisposableHandle.js";
export * from "./AsyncDisposableHandle.js";
export * from "./MultiKeyMap.js";
export * from "./splitText.js";
export * from "./DisposedError.js";
77 changes: 77 additions & 0 deletions test/AsyncDisposableHandle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import {describe, expect, test} from "vitest";
import {AsyncDisposableHandle} from "../src/index.js";

describe("AsyncDisposableHandle", () => {
test("disposing only happens once", async () => {
let disposeTimes = 0;
const handle = new AsyncDisposableHandle(async () => {
disposeTimes++;
});

expect(disposeTimes).toBe(0);
expect(handle.disposed).toBe(false);

await handle.dispose();
expect(disposeTimes).toBe(1);
expect(handle.disposed).toBe(true);

await handle.dispose();
expect(disposeTimes).toBe(1);
expect(handle.disposed).toBe(true);
});

test("marked as disposed before the callback finishes", async () => {
let disposeTimes = 0;
const handle = new AsyncDisposableHandle(async () => {
disposeTimes++;
});

expect(disposeTimes).toBe(0);
expect(handle.disposed).toBe(false);

void handle.dispose();
expect(disposeTimes).toBe(1);
expect(handle.disposed).toBe(true);

void handle.dispose();
expect(disposeTimes).toBe(1);
expect(handle.disposed).toBe(true);
});

test("Symbol.dispose works", async () => {
let disposeTimes = 0;
const handle = new AsyncDisposableHandle(async () => {
disposeTimes++;
});

expect(disposeTimes).toBe(0);
expect(handle.disposed).toBe(false);

await handle[Symbol.asyncDispose]();
expect(disposeTimes).toBe(1);
expect(handle.disposed).toBe(true);

await handle[Symbol.asyncDispose]();
expect(disposeTimes).toBe(1);
expect(handle.disposed).toBe(true);
});

test("Storing dispose function in a variable", () => {
let disposeTimes = 0;
const handle = new AsyncDisposableHandle(async () => {
disposeTimes++;
});

expect(disposeTimes).toBe(0);
expect(handle.disposed).toBe(false);

const dispose = handle.dispose;
dispose();
expect(disposeTimes).toBe(1);
expect(handle.disposed).toBe(true);

dispose();
expect(disposeTimes).toBe(1);
expect(handle.disposed).toBe(true);
});
});
59 changes: 59 additions & 0 deletions test/DisposableHandle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {describe, expect, test} from "vitest";
import {DisposableHandle} from "../src/index.js";

describe("DisposableHandle", () => {
test("disposing only happens once", () => {
let disposeTimes = 0;
const handle = new DisposableHandle(() => {
disposeTimes++;
});

expect(disposeTimes).toBe(0);
expect(handle.disposed).toBe(false);

handle.dispose();
expect(disposeTimes).toBe(1);
expect(handle.disposed).toBe(true);

handle.dispose();
expect(disposeTimes).toBe(1);
expect(handle.disposed).toBe(true);
});

test("Symbol.dispose works", () => {
let disposeTimes = 0;
const handle = new DisposableHandle(() => {
disposeTimes++;
});

expect(disposeTimes).toBe(0);
expect(handle.disposed).toBe(false);

handle[Symbol.dispose]();
expect(disposeTimes).toBe(1);
expect(handle.disposed).toBe(true);

handle[Symbol.dispose]();
expect(disposeTimes).toBe(1);
expect(handle.disposed).toBe(true);
});

test("Storing dispose function in a variable", () => {
let disposeTimes = 0;
const handle = new DisposableHandle(() => {
disposeTimes++;
});

expect(disposeTimes).toBe(0);
expect(handle.disposed).toBe(false);

const dispose = handle.dispose;
dispose();
expect(disposeTimes).toBe(1);
expect(handle.disposed).toBe(true);

dispose();
expect(disposeTimes).toBe(1);
expect(handle.disposed).toBe(true);
});
});

0 comments on commit 3df1087

Please sign in to comment.