feat: init project
This commit is contained in:
339
src/container/Container.ts
Normal file
339
src/container/Container.ts
Normal file
@@ -0,0 +1,339 @@
|
||||
import "reflect-metadata";
|
||||
import { CircularDependencyError, ProviderNotFoundError } from "../errors";
|
||||
import { ContextualModuleRef, ModuleRef } from "../module/ModuleRef";
|
||||
import type { Constructor } from "../types";
|
||||
import {
|
||||
type BaseProvider,
|
||||
INJECT_METADATA_KEY,
|
||||
INJECTABLE_METADATA_KEY,
|
||||
MODULE_METADATA_KEY,
|
||||
type ModuleOptions,
|
||||
type Provider,
|
||||
type ProviderToken,
|
||||
type ResolvedProvider,
|
||||
} from "./types";
|
||||
|
||||
interface ParamType {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
type LifecycleHook = "onModuleInit" | "onModuleDestroy";
|
||||
type LifecycleHookFn = () => Promise<void> | void;
|
||||
type LifecycleAware = Partial<Record<LifecycleHook, LifecycleHookFn>>;
|
||||
|
||||
export class Container {
|
||||
private readonly providers = new Map<
|
||||
string,
|
||||
Map<ProviderToken, ResolvedProvider>
|
||||
>();
|
||||
private readonly instances = new Map<string, Map<ProviderToken, unknown>>();
|
||||
private readonly moduleOptions = new Map<string, ModuleOptions>();
|
||||
private readonly resolving = new Set<ProviderToken>();
|
||||
private rootModuleName?: string;
|
||||
|
||||
constructor(private readonly globalContext = "global") {
|
||||
this.ensureScope(this.globalContext);
|
||||
}
|
||||
|
||||
public registerRootModule(moduleClass: Constructor): void {
|
||||
this.rootModuleName = moduleClass.name;
|
||||
this.registerModule(moduleClass);
|
||||
}
|
||||
|
||||
public getRootModuleName(): string | undefined {
|
||||
return this.rootModuleName;
|
||||
}
|
||||
|
||||
public registerModule(moduleClass: Constructor): void {
|
||||
const options: ModuleOptions | undefined = Reflect.getMetadata(
|
||||
MODULE_METADATA_KEY,
|
||||
moduleClass,
|
||||
);
|
||||
if (!options) return;
|
||||
|
||||
const moduleName = moduleClass.name;
|
||||
if (this.moduleOptions.has(moduleName)) return;
|
||||
|
||||
this.moduleOptions.set(moduleName, options);
|
||||
this.ensureScope(moduleName);
|
||||
|
||||
if (options.imports) {
|
||||
for (const imported of options.imports) {
|
||||
this.registerModule(imported);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.providers) {
|
||||
for (const provider of options.providers) {
|
||||
const normalized = this.normalizeProvider(provider, moduleName);
|
||||
this.providers
|
||||
.get(moduleName)!
|
||||
.set(normalized.token, normalized);
|
||||
}
|
||||
}
|
||||
|
||||
const moduleProvider = this.normalizeProvider(moduleClass, moduleName);
|
||||
this.providers.get(moduleName)!.set(moduleClass, moduleProvider);
|
||||
}
|
||||
|
||||
public registerProvider(provider: Provider): void {
|
||||
const normalized = this.normalizeProvider(provider, this.globalContext);
|
||||
this.providers
|
||||
.get(this.globalContext)!
|
||||
.set(normalized.token, normalized);
|
||||
}
|
||||
|
||||
public async get<T>(
|
||||
token: ProviderToken<T>,
|
||||
moduleName?: string,
|
||||
): Promise<T> {
|
||||
return this.resolve(token, moduleName);
|
||||
}
|
||||
|
||||
public async resolveAll(): Promise<void> {
|
||||
for (const [scope] of this.providers) {
|
||||
for (const token of this.providers.get(scope)!.keys()) {
|
||||
await this.resolve(
|
||||
token,
|
||||
scope === this.globalContext ? undefined : scope,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async callLifecycleHook(
|
||||
hook: "onModuleInit" | "onModuleDestroy",
|
||||
): Promise<void> {
|
||||
for (const instances of this.instances.values()) {
|
||||
for (const instance of instances.values()) {
|
||||
if (!instance) continue;
|
||||
const lifecycleInstance = instance as LifecycleAware;
|
||||
const hookFn = lifecycleInstance[hook];
|
||||
if (typeof hookFn === "function") {
|
||||
await hookFn.call(lifecycleInstance);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async resolve<T>(
|
||||
token: ProviderToken<T>,
|
||||
moduleName?: string,
|
||||
): Promise<T> {
|
||||
const context = moduleName ?? this.globalContext;
|
||||
|
||||
if (token === ModuleRef) {
|
||||
return new ContextualModuleRef(this, context) as T;
|
||||
}
|
||||
|
||||
let provider = this.findProvider(token, context);
|
||||
|
||||
if (
|
||||
!provider &&
|
||||
context === this.globalContext &&
|
||||
this.rootModuleName
|
||||
) {
|
||||
provider = this.findProvider(token, this.rootModuleName);
|
||||
}
|
||||
|
||||
if (!provider) {
|
||||
throw new ProviderNotFoundError(
|
||||
typeof token === "function" ? token.name : String(token),
|
||||
context,
|
||||
);
|
||||
}
|
||||
|
||||
if (provider.scope === "singleton") {
|
||||
const existing = this.instances
|
||||
.get(provider.moduleName ?? context)
|
||||
?.get(provider.token);
|
||||
if (existing !== undefined) {
|
||||
return existing as T;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.resolving.has(token)) {
|
||||
const stack = [...this.resolving].map((item) =>
|
||||
typeof item === "function" ? item.name : String(item),
|
||||
);
|
||||
stack.push(
|
||||
typeof token === "function" ? token.name : String(token),
|
||||
);
|
||||
throw new CircularDependencyError(stack);
|
||||
}
|
||||
|
||||
this.resolving.add(token);
|
||||
|
||||
try {
|
||||
let instance: T;
|
||||
|
||||
if (provider.useValue !== undefined) {
|
||||
instance = provider.useValue as T;
|
||||
} else if (provider.useFactory) {
|
||||
const dependencies = await Promise.all(
|
||||
(provider.inject ?? []).map((dep) =>
|
||||
this.resolve(dep, provider.moduleName ?? context),
|
||||
),
|
||||
);
|
||||
instance = (await provider.useFactory(...dependencies)) as T;
|
||||
} else if (provider.useExisting) {
|
||||
instance = await this.resolve<T>(
|
||||
provider.useExisting as ProviderToken<T>,
|
||||
provider.moduleName ?? context,
|
||||
);
|
||||
} else if (provider.useClass) {
|
||||
const targetClass = provider.useClass;
|
||||
const paramTypes: ParamType[] =
|
||||
Reflect.getMetadata("design:paramtypes", targetClass) || [];
|
||||
const injectTokens: ProviderToken[] =
|
||||
Reflect.getMetadata(INJECT_METADATA_KEY, targetClass) || [];
|
||||
|
||||
this.debug(`[Container] Instantiating ${targetClass.name}`);
|
||||
this.debug(
|
||||
"[Container] ParamTypes:",
|
||||
paramTypes.map((p) => p?.name || String(p)),
|
||||
);
|
||||
this.debug(
|
||||
"[Container] InjectTokens:",
|
||||
injectTokens.map((token) =>
|
||||
token ? String(token) : "undefined",
|
||||
),
|
||||
);
|
||||
|
||||
const dependencies = await Promise.all(
|
||||
paramTypes.map(async (paramType, index) => {
|
||||
const depToken =
|
||||
injectTokens[index] || (paramType as ProviderToken);
|
||||
this.debug(
|
||||
`[Container] Resolving dependency ${index}: ${String(depToken)}`,
|
||||
);
|
||||
return this.resolve(
|
||||
depToken as ProviderToken<unknown>,
|
||||
provider.moduleName ?? context,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
instance = new targetClass(...dependencies) as T;
|
||||
} else {
|
||||
throw new Error(
|
||||
`Invalid provider configuration for token ${String(token)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (provider.scope === "singleton") {
|
||||
const scopeName = provider.moduleName ?? context;
|
||||
this.ensureScope(scopeName);
|
||||
this.instances.get(scopeName)!.set(provider.token, instance);
|
||||
}
|
||||
|
||||
return instance;
|
||||
} finally {
|
||||
this.resolving.delete(token);
|
||||
}
|
||||
}
|
||||
|
||||
private findProvider(
|
||||
token: ProviderToken,
|
||||
context: string,
|
||||
): ResolvedProvider | undefined {
|
||||
const moduleProviders = this.providers.get(context);
|
||||
if (moduleProviders?.has(token)) {
|
||||
return moduleProviders.get(token);
|
||||
}
|
||||
|
||||
const options = this.moduleOptions.get(context);
|
||||
if (options?.imports) {
|
||||
for (const imported of options.imports) {
|
||||
const importedProviders = this.providers.get(imported.name);
|
||||
const importedOptions = this.moduleOptions.get(imported.name);
|
||||
if (
|
||||
importedProviders?.has(token) &&
|
||||
importedOptions?.exports?.includes(token)
|
||||
) {
|
||||
return importedProviders.get(token);
|
||||
}
|
||||
if (token === imported) {
|
||||
return importedProviders?.get(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (context !== this.globalContext) {
|
||||
const globalProviders = this.providers.get(this.globalContext);
|
||||
if (globalProviders?.has(token)) {
|
||||
return globalProviders.get(token);
|
||||
}
|
||||
}
|
||||
|
||||
if (context === this.globalContext && this.rootModuleName) {
|
||||
const rootProviders = this.providers.get(this.rootModuleName);
|
||||
if (rootProviders?.has(token)) {
|
||||
return rootProviders.get(token);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private normalizeProvider(
|
||||
provider: Provider,
|
||||
moduleName?: string,
|
||||
): ResolvedProvider {
|
||||
if (typeof provider === "function") {
|
||||
const metadata = Reflect.getMetadata(
|
||||
INJECTABLE_METADATA_KEY,
|
||||
provider,
|
||||
);
|
||||
return {
|
||||
token: provider,
|
||||
scope: metadata?.scope ?? "singleton",
|
||||
useClass: provider,
|
||||
moduleName,
|
||||
};
|
||||
}
|
||||
|
||||
const base = {
|
||||
token: provider.provide,
|
||||
scope: provider.scope ?? "singleton",
|
||||
moduleName,
|
||||
};
|
||||
|
||||
if ("useClass" in provider) {
|
||||
return { ...base, useClass: provider.useClass };
|
||||
}
|
||||
if ("useValue" in provider) {
|
||||
return { ...base, useValue: provider.useValue };
|
||||
}
|
||||
if ("useFactory" in provider) {
|
||||
return {
|
||||
...base,
|
||||
useFactory: provider.useFactory,
|
||||
inject: provider.inject,
|
||||
};
|
||||
}
|
||||
if ("useExisting" in provider) {
|
||||
return { ...base, useExisting: provider.useExisting };
|
||||
}
|
||||
|
||||
const exhaustiveCheck: never = provider;
|
||||
throw new Error(
|
||||
`Invalid provider definition for token ${String((exhaustiveCheck as BaseProvider).provide)}`,
|
||||
);
|
||||
}
|
||||
|
||||
private ensureScope(name: string): void {
|
||||
if (!this.providers.has(name)) {
|
||||
this.providers.set(name, new Map());
|
||||
}
|
||||
if (!this.instances.has(name)) {
|
||||
this.instances.set(name, new Map());
|
||||
}
|
||||
}
|
||||
|
||||
private debug(...args: unknown[]): void {
|
||||
if (process.env.ALVEO_CONTAINER_DEBUG === "true") {
|
||||
console.debug(...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user