wardens
Version:
A framework for resource management
129 lines (109 loc) • 3.7 kB
text/typescript
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,
);
}
}