provider is key

This commit is contained in:
Robin Chappatte
2024-06-10 16:27:21 +02:00
parent 9bb3e28b34
commit b654a3b26f
6 changed files with 218 additions and 158 deletions

View File

@@ -1,75 +1,58 @@
type Provider<T> = (manager: DependencyManager) => Promise<T> | T;
export type Provider<T = unknown> = (manager: DependencyManager) => T;
// deno-lint-ignore no-explicit-any
type HasConstructor<T> = T extends new (...args: any[]) => unknown ? T : never;
// deno-lint-ignore no-explicit-any
type ConstructorInstance<T> = T extends new (...args: any[]) => infer I ? I
: never;
// deno-lint-ignore no-explicit-any
type ModuleIdentifier = any;
type Modules = Map<ModuleIdentifier, {
// deno-lint-ignore no-explicit-any
provider: Provider<any>;
// deno-lint-ignore no-explicit-any
instance?: any;
}>;
export type ProviderGroup = {
[key: string]: Provider | ProviderGroup;
};
export class DependencyManager {
private modules: Modules = new Map();
private resolving: Set<ModuleIdentifier> = new Set();
private declarations = new Map<unknown, Provider>();
private instances = new Map();
private resolving = new Set();
/**
* Set the given provider to be used to resolve the dependency identified by the given identifier.
*
* Throw an error if the identifier is already used.
*
* @param identifier the identifier that will be used to ask the manager for the resolved dependency.
* @param provider a function that receive the dependency manager as param and return the dependency. Can be asynchronous.
*/
register<T>(identifier: ModuleIdentifier, provider: Provider<T>): void {
if (this.modules.has(identifier)) {
throw new Error(`Module ${identifier} is already registered.`);
}
this.modules.set(identifier, { provider });
}
register(dependency: Provider): void;
register(module: ProviderGroup): void;
register(dependencyOrModule: Provider | ProviderGroup): void;
register(dependencyOrModule: Provider | ProviderGroup) {
if (this.declarations.has(dependencyOrModule)) {
throw new Error('This dependency has already been registred');
}
/**
* Return the dependency matching the given identifier.
* If the dependency has not been resolved yet, resolve it first.
*
* Throw an error if:
* - no provider has been registred with that identifier
* - a circular dependency is detected
*
* @param identifier the identifier used to register the provider
*/
async resolve<T>(
identifier: HasConstructor<T>,
): Promise<ConstructorInstance<T>>;
async resolve<T>(identifier: ModuleIdentifier): Promise<T>;
async resolve(identifier: ModuleIdentifier) {
const module = this.modules.get(identifier);
if (!module) {
throw new Error(`Module ${identifier} is not registered.`);
}
if (typeof dependencyOrModule === 'function') {
this.declarations.set(dependencyOrModule, dependencyOrModule);
return;
}
if (module.instance) {
return module.instance;
}
const module = dependencyOrModule;
for (const key in module) {
this.register(module[key]);
}
}
if (this.resolving.has(identifier)) {
throw new Error(`Circular dependency detected for module ${identifier}.`);
}
resolve<T>(dependency: Provider<T>): T {
if (!this.declarations.has(dependency)) {
throw new Error('This dependency has not been registred');
}
this.resolving.add(identifier);
if (this.instances.has(dependency)) {
return this.instances.get(dependency) as T;
}
try {
module.instance = await module.provider(this);
return module.instance;
} finally {
this.resolving.delete(identifier);
}
}
if (this.resolving.has(dependency)) {
throw new Error('Circular dependency detected');
}
this.resolving.add(dependency);
const instance = this.instanciate(dependency);
this.resolving.delete(dependency);
return instance;
}
private instanciate<T>(dependency: Provider<T>): T {
const declaration = this.declarations.get(dependency) as Provider<T>;
const instance = declaration(this);
this.instances.set(dependency, instance);
return instance;
}
}