Skip to content

Instantly share code, notes, and snippets.

@nberlette
Last active October 31, 2023 05:20
Show Gist options
  • Save nberlette/678f2bac2b87b710586d11e9891673f1 to your computer and use it in GitHub Desktop.
Save nberlette/678f2bac2b87b710586d11e9891673f1 to your computer and use it in GitHub Desktop.
[TS] DisposableStack + AsyncDisposableStack Implementation (explicit resource management)

Explicit Resource Management Polyfill

This is a bare-bones implementation of the interfaces AsyncDisposableStack and DisposableStack from the Explicit Resource Management Proposal.

Usage

Note: see the examples below for more detailed usage examples.

DisposableStack and Disposable

import {
  Disposable,
  DisposableStack,
} from "https://gist.githubusercontent.com/nberlette/678f2bac2b87b710586d11e9891673f1/raw/mod.ts";

AsyncDisposableStack and AsyncDisposable

import {
  AsyncDisposable,
  AsyncDisposableStack,
} from "https://gist.githubusercontent.com/nberlette/678f2bac2b87b710586d11e9891673f1/raw/mod.ts";

Symbol.dispose and Symbol.asyncDispose

If you're in an environment that doesn't yet support the Symbol.dispose and Symbol.asyncDispose symbols, you can use the polyfill provided in this gist to add them to the global Symbol object. It includes global type definitions that are augmented into the SymbolConstructor interface.

import "https://gist.githubusercontent.com/nberlette/678f2bac2b87b710586d11e9891673f1/raw/symbol.ts";

Note: You have to explicitly import the ./symbol.ts file, however, as it is not exported from the ./mod.ts file.

This is to prevent type collisions in environments that already support the proposed symbol. There can only be one unique symbol for a given name in any scope (that's what makes them unique!)


Examples

AsyncDisposableStack

Here's a simple example of how one might use the AsyncDisposableStack API. You can drop this code into the Deno CLI and it should "just work".

import {
  AsyncDisposable,
  AsyncDisposableStack,
} from "https://gist.githubusercontent.com/nberlette/678f2bac2b87b710586d11e9891673f1/raw/mod.ts";

class AsyncConstruct implements AsyncDisposable {
  #resourceA: AsyncDisposable;
  #resourceB: AsyncDisposable;
  #resources: AsyncDisposableStack;

  get resourceA() { return this.#resourceA; }

  get resourceB() { return this.#resourceB; }

  async init(): Promise<void> {
    // stack will be disposed when exiting this method for any reason
    await using stack = new AsyncDisposableStack();

    // adopts an async resource, adding it to the stack. this lets us utilize
    // resource management APIs with existing features that may not support the
    // bleeding-edge features like `AsyncDisposable` yet. In this case, we're
    // adding a temporary file (as a string), with a removal function that will
    // clean up the file when the stack is disposed (or this function exits).
    this.#resourceA = await stack.adopt(await Deno.makeTempFile(), async (path) => await Deno.remove(path));

    // do some work with the resource
    await Deno.writeTextFile(this.#resourceA, JSON.stringify({ foo: "bar" }));

    // Acquire a second resource. If this fails, both `stack` and `#resourceA`
    // will be disposed. Notice we use the `.use` method here, since we're
    // acquiring a resource that implements the `AsyncDisposable` interface.
    this.#resourceB = await stack.use(await this.get());

    // all operations succeeded, move resources out of `stack` so that they aren't disposed
    // when this function exits. we can now use the resources as we please, and
    // they will be disposed when the parent object is disposed.
    this.#resources = stack.move();
  }

  async get(): Promise<AsyncDisposable> {
    console.log("🅱️ acquiring resource B")
    const resource = { data: JSON.parse(await Deno.readTextFile(this.#resourceA)) };
    return Object.create({
      async [Symbol.asyncDispose]() {
        console.log("🅱️ disposing resource B")
        resource.data = null!;
        return await Promise.resolve();
      },
    }, { resource: { value: resource, enumerable: true } });
  }

  async disposeAsync() {
    await this.#resources.disposeAsync();
  }

  async [Symbol.asyncDispose]() {
    await this.#resources.disposeAsync();
  }
}

{
  await using construct = new AsyncConstruct();
  await construct.init();
  console.log("🅰️ resource A:", construct.resourceA);
  console.log("🅱️ resource B:", construct.resourceB);
  console.log("We're done here.");
}

DisposableStack

class Construct implements Disposable {
  #resourceA: Disposable;
  #resourceB: Disposable;
  #resources: DisposableStack;
  constructor() {
    // stack will be disposed when exiting constructor for any reason
    using stack = new DisposableStack();

    // get first resource
    this.#resourceA = stack.use(getResource1());

    // get second resource. If this fails, both `stack` and `#resourceA` will be disposed.
    this.#resourceB = stack.use(getResource2());

    // all operations succeeded, move resources out of `stack` so that they aren't disposed
    // when constructor exits
    this.#resources = stack.move();
  }

  [Symbol.dispose]() {
    this.#resources.dispose();
  }
}

MIT © Nicholas Berlette. All rights reserved.

This polyfill is based on the Explicit Resource Management Proposal by TC39.
It was implemented using the TypeScript v5.2.2 type definitions as a reference point.
/*!
* AsyncDisposableStack Implementation
* MIT License (c) 2023 Nicholas Berlette (https://github.com/nberlette)
*/
/**
* Represents a collection of asynchronous disposable resources. From the
* explicit resource management proposal, based on the TypeScript interface of
* the same name.
*
* @see https://github.com/tc39/proposal-explicit-resource-management
* @see https://github.com/microsoft/TypeScript/blob/v5.2.2/src/lib/esnext.disposable.d.ts
*
* @example
* ```ts
* class C {
* #res1: Disposable;
* #res2: Disposable;
* #disposables: DisposableStack;
* constructor() {
* // stack will be disposed when exiting constructor for any reason
* using stack = new DisposableStack();
*
* // get first resource
* this.#res1 = stack.use(getResource1());
*
* // get second resource. If this fails, both `stack` and `#res1` will be disposed.
* this.#res2 = stack.use(getResource2());
*
* // all operations succeeded, move resources out of `stack` so that they aren't disposed
* // when constructor exits
* this.#disposables = stack.move();
* }
*
* [Symbol.dispose]() {
* this.#disposables.dispose();
* }
* }
* ```
*/
export class AsyncDisposableStack {
#disposed = false;
#stack: [AsyncDisposable | Disposable | undefined, ((value?: unknown) => PromiseLike<void> | void) | undefined][] = [];
#errors: unknown[] = [];
/** Returns a value indicating whether this stack has been disposed. */
get disposed(): boolean {
return this.#disposed;
}
/** Disposes all of its resources in reverse order that they were added. */
async disposeAsync(): Promise<void> {
if (!this.disposed) {
this.#disposed = true;
while (this.#stack.length > 0) {
const [value, onDisposeAsync] = this.#stack.pop()!;
try {
if (typeof onDisposeAsync === "function") {
await onDisposeAsync(value)
}
if (
value && typeof value === "object" ||
typeof value === "function"
) {
if (
Symbol.asyncDispose in value &&
typeof value?.[Symbol.asyncDispose] === "function"
) {
await value[Symbol.asyncDispose]();
} else if (
Symbol.dispose in value &&
typeof value?.[Symbol.dispose] === "function"
) {
await value[Symbol.dispose]();
}
}
} catch (error) {
this.#errors.push(error);
}
}
}
}
/**
* Adds a disposable resource to the stack, returning the resource.
*
* @param value The resource to add. `null` and `undefined` will not be added, but **_will_** be returned.
* @returns The provided {@link value}.
*/
use<T extends AsyncDisposable | Disposable | null | undefined>(value: T): T {
if (this.disposed) throw new ReferenceError("Object has been disposed.");
if (value != null) this.#stack.push([value, undefined]);
return value;
}
/**
* Adds a value and associated disposal callback as a resource to the stack.
* @param value The value to add.
* @param onDisposeAsync The callback to use in place of a `[Symbol.dispose]()` method. Will be invoked with `value` as the first parameter.
* @returns The provided {@link value}.
*/
adopt<T>(value: T, onDisposeAsync: (value: T) => PromiseLike<void> | void): T {
if (this.disposed) throw new ReferenceError("Object has been disposed.");
this.#stack.push([undefined, async () => await onDisposeAsync(value)]);
return value;
}
/**
* Adds a callback to be invoked when the stack is disposed.
*/
defer(onDisposeAsync: () => PromiseLike<void> | void): void {
if (this.disposed) throw new ReferenceError("Object has been disposed.");
this.#stack.push([undefined, onDisposeAsync]);
}
/**
* Move all resources out of this stack and into a new `DisposableStack`, and
* marks this stack as disposed.
*
* @example
* ```ts
* class C {
* #res1: Disposable;
* #res2: Disposable;
* #disposables: DisposableStack;
* constructor() {
* // stack will be disposed when exiting constructor for any reason
* using stack = new DisposableStack();
*
* // get first resource
* this.#res1 = stack.use(getResource1());
*
* // get second resource. If this fails, both `stack` and `#res1` will be disposed.
* this.#res2 = stack.use(getResource2());
*
* // all operations succeeded, move resources out of `stack` so that they aren't disposed
* // when constructor exits
* this.#disposables = stack.move();
* }
*
* [Symbol.dispose]() {
* this.#disposables.dispose();
* }
* }
* ```
*/
move(): AsyncDisposableStack {
if (this.disposed) throw new ReferenceError("Object has been disposed.");
const stack = new AsyncDisposableStack();
stack.#stack = this.#stack;
stack.#disposed = this.#disposed;
this.#stack = [];
this.#disposed = true;
return stack;
}
async [Symbol.asyncDispose](): Promise<void> {
return await this.disposeAsync();
}
declare readonly [Symbol.toStringTag]: string;
static {
Object.defineProperties(this.prototype, {
[Symbol.toStringTag]: {
value: "AsyncDisposableStack",
writable: false,
enumerable: false,
configurable: true,
},
});
}
}
export interface AsyncDisposable {
[Symbol.asyncDispose](): PromiseLike<void>;
}
/*!
* DisposableStack Implementation
* MIT License (c) 2023 Nicholas Berlette (https://github.com/nberlette)
*/
/**
* Represents a collection of disposable resources. From the explicit resource
* management proposal and based on the TypeScript interface of the same name.
*
* @see https://github.com/tc39/proposal-explicit-resource-management
* @see https://github.com/microsoft/TypeScript/blob/v5.2.2/src/lib/esnext.disposable.d.ts
*
* @example
* ```ts
* class C {
* #res1: Disposable;
* #res2: Disposable;
* #disposables: DisposableStack;
* constructor() {
* // stack will be disposed when exiting constructor for any reason
* using stack = new DisposableStack();
*
* // get first resource
* this.#res1 = stack.use(getResource1());
*
* // get second resource. If this fails, both `stack` and `#res1` will be disposed.
* this.#res2 = stack.use(getResource2());
*
* // all operations succeeded, move resources out of `stack` so that they aren't disposed
* // when constructor exits
* this.#disposables = stack.move();
* }
*
* [Symbol.dispose]() {
* this.#disposables.dispose();
* }
* }
* ```
*/
export class DisposableStack {
#disposed = false;
#errors: unknown[] = [];
#stack: [
Disposable | AsyncDisposable | undefined,
((value?: unknown) => void | Promise<void>) | undefined,
][] = [];
/** Returns a value indicating whether this stack has been disposed. */
get disposed(): boolean {
return this.#disposed;
}
/** Disposes all of its resources in reverse order that they were added. */
dispose(): void {
if (!this.disposed) {
this.#disposed = true;
while (this.#stack.length > 0) {
const [value, onDispose] = this.#stack.pop()!;
try {
if (typeof onDispose === "function") onDispose(value);
if (
value && typeof value === "object" ||
typeof value === "function"
) {
if (
Symbol.dispose in value &&
typeof value?.[Symbol.dispose] === "function"
) {
value[Symbol.dispose]();
} else if (
Symbol.asyncDispose in value &&
typeof value?.[Symbol.asyncDispose] === "function"
) {
value[Symbol.asyncDispose]();
}
}
} catch (error) {
this.#errors.push(error);
}
}
}
}
/**
* Adds a disposable resource to the stack, returning the resource.
*
* @param value The resource to add. `null` and `undefined` will not be added, but **_will_** be returned.
* @returns The provided {@link value}.
*/
use<T extends Disposable | AsyncDisposable | null | undefined>(value: T): T {
if (this.disposed) throw new ReferenceError("Object has been disposed.");
if (value != null) this.#stack.push([value, undefined]);
return value;
}
/**
* Adds a value and associated disposal callback as a resource to the stack.
* @param value The value to add.
* @param onDispose The callback to use in place of a `[Symbol.dispose]()` method. Will be invoked with `value` as the first parameter.
* @returns The provided {@link value}.
*/
adopt<T>(value: T, onDispose: (value: T) => void): T {
if (this.disposed) throw new ReferenceError("Object has been disposed.");
this.#stack.push([undefined, () => onDispose(value)]);
return value;
}
/**
* Adds a callback to be invoked when the stack is disposed.
*/
defer(onDispose: () => void): void {
if (this.disposed) throw new ReferenceError("Object has been disposed.");
this.#stack.push([undefined, onDispose]);
}
/**
* Move all resources out of this stack and into a new `DisposableStack`, and
* marks this stack as disposed.
*
* @example
* ```ts
* class C {
* #res1: Disposable;
* #res2: Disposable;
* #disposables: DisposableStack;
* constructor() {
* // stack will be disposed when exiting constructor for any reason
* using stack = new DisposableStack();
*
* // get first resource
* this.#res1 = stack.use(getResource1());
*
* // get second resource. If this fails, both `stack` and `#res1` will be disposed.
* this.#res2 = stack.use(getResource2());
*
* // all operations succeeded, move resources out of `stack` so that they aren't disposed
* // when constructor exits
* this.#disposables = stack.move();
* }
*
* [Symbol.dispose]() {
* this.#disposables.dispose();
* }
* }
* ```
*/
move(): DisposableStack {
if (this.disposed) throw new ReferenceError("Object has been disposed.");
const stack = new DisposableStack();
stack.#stack = this.#stack;
stack.#disposed = this.#disposed;
this.#stack = [];
this.#disposed = true;
return stack;
}
[Symbol.dispose](): void {
this.dispose();
}
declare readonly [Symbol.toStringTag]: string;
static {
Object.defineProperties(this.prototype, {
[Symbol.toStringTag]: {
value: "DisposableStack",
writable: false,
enumerable: false,
configurable: true,
},
});
}
}
export interface Disposable {
[Symbol.dispose](): void;
}
export * from "./async-disposable-stack.ts";
export * from "./async-disposable.ts";
export * from "./disposable-stack.ts";
export * from "./disposable.ts";
/// <reference lib="es2015.symbol" />
declare global {
interface SymbolConstructor {
/**
* A method that is used to release resources held by an object. Called by
* the semantics of the `using` statement.
*/
readonly dispose: unique symbol;
/**
* A method that is used to asynchronously release resources held by an
* object. Called by the semantics of the `await using` statement.
*/
readonly asyncDispose: unique symbol;
}
}
if (typeof Symbol === "function" && typeof Symbol.dispose !== "symbol") {
Object.defineProperty(globalThis.Symbol, "dispose", {
value: Symbol("Symbol.dispose"),
enumerable: false,
configurable: false,
writable: false,
});
}
if (typeof Symbol === "function" && typeof Symbol.asyncDispose !== "symbol") {
Object.defineProperty(globalThis.Symbol, "asyncDispose", {
value: Symbol("Symbol.asyncDispose"),
enumerable: false,
configurable: false,
writable: false,
});
}
export {};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment