UNPKG

wardens

Version:

A framework for resource management

129 lines (109 loc) 3.7 kB
import { resources, constructed } from './global-weakrefs'; import wrap from './wrap-with-proxy'; import ResourceControls from './resource-controls'; import type { InheritedContext } from './inherited-context'; import { ResourceFactory, ParametrizedResourceFactory } from './utility-types'; /** Provision a resource and return its external API. */ export const createWithContext = async < Controls extends object, Args extends Array<unknown>, >( state: InheritedContext, factory: | ParametrizedResourceFactory<Controls, Args> | ResourceFactory<Controls>, ...args: Args ): Promise<Controls> => { const curfew = { enforced: false }; const children: Set<object> = new Set(); const context = new ResourceControls(state, children, curfew); let resource: Awaited<ReturnType<typeof factory>>; try { resource = await factory(context, ...args); } catch (error) { // Resource could not be created. Clean up the intermediate resources. const orphans = Array.from(children).reverse(); const deallocations = await Promise.allSettled( orphans.map((orphan) => context.destroy(orphan)), ); const failures = deallocations.filter( (result): result is PromiseRejectedResult => result.status === 'rejected', ); // The intermediate resources could not be destroyed. if (failures.length) { throw reduceToSingleError( failures.map((failure) => failure.reason), { cause: error }, ); } throw error; } const controls = resource.value; const { proxy, revoke } = wrap(controls); constructed.add(proxy); resources.set(proxy, { curfew, resource, children, revoke, }); return proxy; }; /** * Tear down the resource and all its children, permanently destroying the * reference. */ export const destroy = async (handle: object) => { if (!constructed.has(handle)) { throw new Error('Cannot destroy object. It is not a resource.'); } const entry = resources.get(handle); if (entry) { // Instantly delete to prevent race conditions. resources.delete(handle); // Free all references. entry.revoke(); let parentDeallocation: PromiseSettledResult<void> = { status: 'fulfilled', value: undefined, }; // Try to close the parent resource... if (entry.resource.destroy) { try { await entry.resource.destroy(); } catch (error) { parentDeallocation = { status: 'rejected', reason: error }; } } // The resource is closed. Prevent new child resources before we start // closing down the children. entry.curfew.enforced = true; // Recursively close out the children... const recursiveUnmounts = Array.from(entry.children).reverse().map(destroy); const deallocations = await Promise.allSettled(recursiveUnmounts); const failures = [parentDeallocation] .concat(deallocations) .filter( (result): result is PromiseRejectedResult => result.status === 'rejected', ); // Fail loudly if any of the children couldn't be deallocated. if (failures.length) { throw reduceToSingleError(failures.map((failure) => failure.reason)); } } }; const reduceToSingleError = (errors: Array<Error>, options?: ErrorOptions) => { return errors.length === 1 ? errors[0] : new BulkDestroyError(errors, options); }; /** Happens when 2 or more child resources cannot be destroyed. */ class BulkDestroyError extends Error { constructor(public failures: Array<unknown>, options?: ErrorOptions) { super( 'Some resources could not be destroyed. See the `failures` property for details.', options, ); } }