diff --git a/disposal.ts b/disposal.ts index 6b972e6..a562b19 100644 --- a/disposal.ts +++ b/disposal.ts @@ -10,6 +10,24 @@ * @module */ +import type { + AbortablePromiseWithDisposal, + ReadableStreamWithDisposal, + PromiseWithDisposal, + WithDisposal, +} from "./types.ts"; +import { isAsyncDisposable, isAsyncIterable, isDisposable, isIterable } from "./utils.ts"; + +import { AsyncDisposableStack as _AsyncDisposableStackPolyfill } from "@nick/dispose/async-disposable-stack"; +import { DisposableStack as _DisposableStackPollyfill } from "@nick/dispose/disposable-stack"; + +export const AsyncDisposableStack = "AsyncDisposableStack" in globalThis ? + globalThis.AsyncDisposableStack : + _AsyncDisposableStackPolyfill; +export const DisposableStack = "DisposableStack" in globalThis ? + globalThis.DisposableStack : + _DisposableStackPollyfill; + /** * A WeakMap that stores the `ReadableStreamDefaultReader` for a given `ReadableStream`. * @@ -157,6 +175,135 @@ export function withDisposal( }); } +/** + * Handles a single value that might be disposable or async-disposable. + * @param value - A single value that may or may not be disposable. + * @param stack - The stack used to manage disposal. + * @returns The same value wrapped with disposal management if applicable. + */ +export function useDisposableStack(value: T, stack: DisposableStack | AsyncDisposableStack): T | WithDisposal; + +/** + * Handles a synchronous iterable of values that might be disposable or async-disposable. + * @param iterable - A synchronous iterable of values that may or may not be disposable. + * @param stack - The stack used to manage disposal. + * @returns A synchronous iterable where each value is wrapped with disposal management if applicable. + */ +export function useDisposableStack(iterable: Iterable, stack: DisposableStack | AsyncDisposableStack): Iterable>; + +/** + * Handles an asynchronous iterable of values that might be disposable or async-disposable. + * @param iterable - An asynchronous iterable of values that may or may not be disposable. + * @param stack - The stack used to manage disposal. + * @returns A Promise resolving to an asynchronous iterable where each value is wrapped with disposal management if applicable. + */ +export function useDisposableStack(iterable: AsyncIterable, stack: DisposableStack | AsyncDisposableStack): Promise>>; + +/** + * `useDisposableStack` is a utility function designed to integrate a singular value or an iterable collection + * of resources with a `DisposableStack` or `AsyncDisposableStack`. This function is particularly useful + * for managing resources that require explicit disposal, allowing for both synchronous and asynchronous + * cleanup operations. + * + * The function handles various input types, including single values, synchronous iterables, and asynchronous + * iterables. It ensures that resources implementing `Disposable`, `AsyncDisposable`, or `DualDisposable` are + * properly managed and disposed of by the provided stack. + * + * By utilizing `useDisposableStack`, you can avoid resource leaks and maintain fine-grained control over + * resource management, especially in contexts where disposal needs to be a separate and explicit action. + * This is particularly important when using the `DisposableStack` in a wrapper function, where you'd want + * all resources to be disposed of explicitly at the appropriate time. + * + * @example + * ```typescript + * // Using with a synchronous iterable + * const stack = new DisposableStack(); + * const resources = [new Resource1(), new Resource2()]; + * + * for (const resource of useDisposableStack(resources, stack)) { + * // Use resource + * // The resource will be automatically managed by the stack and disposed of when appropriate. + * } + * + * // Using with an asynchronous iterable + * const asyncStack = new AsyncDisposableStack(); + * const asyncResources = [new AsyncResource1(), new AsyncResource2()]; + * + * for await (const asyncResource of useDisposableStack(asyncResources, asyncStack)) { + * // Use asyncResource + * // The asyncResource will be automatically managed by the async stack and disposed of when appropriate. + * } + * + * // Using with a single disposable value + * const singleResource = new Resource1(); + * const managedResource = useDisposableStack(singleResource, stack); + * // managedResource is now managed by the stack. + * ``` + * + * @template T - The type of the value or items in the iterable. + * @param value - A single value, or an iterable collection of resources that may or may not be disposables. + * @param stack - A stack that manages the disposal of resources. + * @returns Returns the value or iterable, with resources wrapped in disposal management if applicable. + */ +export function useDisposableStack( + value: T | AsyncIterable | Iterable, + stack: DisposableStack | AsyncDisposableStack +): T | WithDisposal | Iterable> | Promise>> { + const isDisposableStack = stack instanceof DisposableStack; + const isAsyncDisposableStack = stack instanceof AsyncDisposableStack; + + // Handle single synchronous disposable value + if (isDisposableStack && isDisposable(value)) { + return stack.use(value as T & Disposable); + } + + // Handle single asynchronous disposable value + if (isAsyncDisposableStack && isAsyncDisposable(value)) { + return stack.use(value as T & AsyncDisposable); + } + + // Handle asynchronous iterables + if (isAsyncIterable(value)) { + /** + * @todo Use Iterator helper function to handle mapping over async-iterables if this is ever standardized available + */ + return Array.fromAsync(value, (result) => { + if (isDisposableStack && isDisposable(result)) { + return stack.use(result as T & Disposable); + } + + if (isAsyncDisposableStack && isAsyncDisposable(result)) { + return stack.use(result as T & AsyncDisposable); + } + + return result; + }); + } + + // Handle synchronous iterables + if (isIterable(value)) { + /** + * @todo Use Iterator helper function to handle mapping over iterables + * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator#iterator_helpers) + */ + return Array.from(value).map((result) => { + if (isDisposableStack && isDisposable(result)) { + return stack.use(result as T & Disposable); + } + + if (isAsyncDisposableStack && isAsyncDisposable(result)) { + return stack.use(result as T & AsyncDisposable); + } + + return result; + }); + } + + // Return the value as-is if it doesn't require disposal management + return value; +} + + /** * Creates a promise that can be aborted using an `AbortController` or `AbortSignal`. * @@ -189,7 +336,7 @@ export function withDisposal( */ export function abortable( abort: AbortController | AbortSignal, -): AbortablePromiseWithDisposable { +): AbortablePromiseWithDisposal { // Create a promise with external resolve and reject capabilities const { promise, reject } = Promise.withResolvers(); let abortController: AbortController | null = abort instanceof AbortController @@ -286,7 +433,7 @@ export function abortable( export function timeout( ms: number, abort?: AbortController | AbortSignal, -): PromiseWithDisposable { +): PromiseWithDisposal { // Create a promise with external resolve and reject capabilities const { promise, reject } = Promise.withResolvers(); @@ -314,114 +461,5 @@ export function timeout( await abortPromise?.[Symbol.asyncDispose]?.(); }, }, - ) as PromiseWithDisposable; -} - -/** - * `WithDisposable` interface combines the capabilities of both `Disposable` and `AsyncDisposable` interfaces. - * - * This interface is used for objects that require explicit resource management, typically for cleaning up - * resources such as file handles, database connections, or any other resources that need to be disposed - * of when no longer in use. - * - * @example - * ```typescript - * class MyResource implements WithDisposable { - * [Symbol.dispose]() { - * // Synchronous cleanup logic - * } - * - * async [Symbol.asyncDispose]() { - * // Asynchronous cleanup logic - * } - * } - * - * const resource = new MyResource(); - * - * // Ensure resource is disposed synchronously - * using (resource) { - * // Work with resource - * } - * - * // Ensure resource is disposed asynchronously - * await using (await resource) { - * // Work with resource asynchronously - * } - * ``` - * - * The `Symbol.dispose` method will be invoked automatically when the scope in which the `using` keyword is used - * is exited, ensuring that resources are properly released. Similarly, `Symbol.asyncDispose` will be called - * for asynchronous disposal. - * - * @interface - * @extends Disposable - * @extends AsyncDisposable - */ -export interface WithDisposable extends Disposable, AsyncDisposable {} - -/** - * An interface representing a `ReadableStream` with added disposal capabilities. - * This interface extends the `ReadableStream` and includes methods for both - * synchronous and asynchronous disposal of the stream. - * - * @template T - The type of data in the `ReadableStream`. - */ -export interface ReadableStreamWithDisposal - extends ReadableStream, WithDisposable {} - -/** - * A `PromiseWithDisposable` is an extension of the standard `Promise` interface, designed to include - * the ability to clean up resources once the promise is no longer needed or has completed its operation. - * - * ## What is a Disposable? - * - * A **disposable** is an object that implements the `Disposable` and/or `AsyncDisposable` interfaces, - * providing a standard way to release or clean up resources, such as memory or file handles, when they - * are no longer needed. This is particularly important in scenarios where failing to release resources - * can lead to memory leaks or other performance issues. - * - * The `Disposable` interface typically includes a `dispose` method, which can be called to perform - * synchronous cleanup. The `AsyncDisposable` interface includes an `asyncDispose` method, which is - * used for asynchronous cleanup operations. - * - * When using a `PromiseWithDisposable`, you can be confident that any associated resources will be - * properly cleaned up once the promise is settled (resolved or rejected) or when it's manually disposed of. - * This makes it particularly useful in scenarios where promises represent operations tied to external - * resources, such as file I/O, network requests, or UI components. - * - * @template T - The type of the value that the promise resolves to. - * - * @example - * ```typescript - * // Create a disposable promise - * const disposablePromise: PromiseWithDisposable = someAsyncOperation(); - * - * // Use the promise as you would any other promise - * disposablePromise.then(result => console.log(result)); - * - * // When done, dispose of the promise to clean up resources - * disposablePromise[Symbol.dispose](); - * ``` - * - * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise Promise Documentation} - * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/dispose Symbol.dispose Documentation} - */ -export interface PromiseWithDisposable extends Promise, WithDisposable {} - -/** - * Extends `PromiseWithDisposable` with additional properties for handling abortable operations. - * - * @template T - The type of the value that the promise resolves to. - */ -export interface AbortablePromiseWithDisposable - extends PromiseWithDisposable { - /** - * The `AbortSignal` associated with this promise, which allows the promise to be aborted. - */ - signal: AbortSignal | null; - - /** - * The `AbortController` used to control the abort signal. If the signal was passed in, this will be `null`. - */ - controller: AbortController | null; -} + ) as PromiseWithDisposal; +} \ No newline at end of file