blob: 48a5241dfbf54c964324254a622a28565354d117 [file] [log] [blame] [edit]
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
// A finalizable object has a single method `finalize` that is called when
// the object is no longer needed and should clean itself up.
export interface Finalizable {
finalize(): void;
}
// A factory can take a partially created TContext and generate a property
// for a given key on that TContext.
export type Factory<TContext, K extends keyof TContext> = (
ctx: Partial<TContext>
) => TContext[K] & Finalizable;
// A registry contains a factory for each key in TContext.
export type Registry<TContext> = {
[P in keyof TContext]: Factory<TContext, P>;
} & Record<string, (_: TContext) => Finalizable>;
// Creates a context given a registry.
export function create<TContext>(
registry: Registry<TContext>
): TContext & Finalizable {
const context: Partial<TContext> & Finalizable = {
finalize() {
for (const key of Object.getOwnPropertyNames(registry)) {
const name = key as keyof TContext;
try {
if (this[name]) {
(this[name] as unknown as Finalizable).finalize();
}
} catch (e) {
console.info(`Failed to finalize ${String(name)}`);
throw e;
}
}
},
} as Partial<TContext> & Finalizable;
const initialized: Map<keyof TContext, Finalizable> = new Map<
keyof TContext,
Finalizable
>();
for (const key of Object.keys(registry)) {
const name = key as keyof TContext;
const factory = registry[name];
let initializing = false;
Object.defineProperty(context, name, {
configurable: true, // Tests can mock properties
get() {
if (!initialized.has(name)) {
// Notice that this is the getter for the property in question.
// It is possible that during the initialization of one property,
// another property is required. This extra check ensures that
// the construction of propertiers on Context are not circularly
// dependent.
if (initializing) throw new Error(`Circular dependency for ${key}`);
try {
initializing = true;
initialized.set(name, factory(context));
} catch (e) {
console.error(`Failed to initialize ${String(name)}`, e);
} finally {
initializing = false;
}
}
return initialized.get(name);
},
});
}
return context as TContext & Finalizable;
}