feat: enhance asynchronous provider support and fix concurrent resolution bug

Allows factory providers to be asynchronous and ensures concurrent requests for the same singleton await the same promise instead of triggering circular dependency errors.
This commit is contained in:
M1000fr
2026-01-11 17:11:41 +01:00
parent aa8493baf6
commit 536fcfc336
2 changed files with 399 additions and 47 deletions

View File

@@ -30,6 +30,10 @@ export class Container {
private readonly moduleOptions = new Map<string, ModuleOptions>();
private readonly resolving = new Set<ProviderToken>();
private readonly proxies = new Map<string, Map<ProviderToken, unknown>>();
private readonly pendingResolutions = new Map<
string,
Map<ProviderToken, Promise<unknown>>
>();
private rootModuleName?: string;
constructor(private readonly globalContext = "global") {
@@ -160,6 +164,13 @@ export class Container {
if (existing !== undefined) {
return existing as T;
}
const pending = this.pendingResolutions
.get(scopeName)
?.get(unwrappedToken);
if (pending) {
return pending as Promise<T>;
}
}
if (this.resolving.has(unwrappedToken)) {
@@ -185,60 +196,87 @@ export class Container {
throw new CircularDependencyError(stack);
}
this.resolving.add(unwrappedToken);
const performResolution = async (): Promise<T> => {
this.resolving.add(unwrappedToken);
try {
let instance: T;
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) || [];
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) ||
[];
const dependencies = await Promise.all(
paramTypes.map(async (paramType, index) => {
const depToken =
injectTokens[index] || (paramType as ProviderToken);
return this.resolve(
depToken as ProviderToken<unknown>,
provider.moduleName ?? context,
);
}),
);
const dependencies = await Promise.all(
paramTypes.map(async (paramType, index) => {
const depToken =
injectTokens[index] ||
(paramType as ProviderToken);
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)}`,
);
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(unwrappedToken, instance);
}
return instance;
} finally {
this.resolving.delete(unwrappedToken);
if (provider.scope === "singleton") {
const scopeName = provider.moduleName ?? context;
this.pendingResolutions
.get(scopeName)
?.delete(unwrappedToken);
}
}
};
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(unwrappedToken);
if (provider.scope === "singleton") {
const scopeName = provider.moduleName ?? context;
this.ensureScope(scopeName);
const promise = performResolution();
this.pendingResolutions
.get(scopeName)!
.set(unwrappedToken, promise);
return promise;
}
return performResolution();
}
private findProvider(
@@ -351,6 +389,9 @@ export class Container {
if (!this.proxies.has(name)) {
this.proxies.set(name, new Map());
}
if (!this.pendingResolutions.has(name)) {
this.pendingResolutions.set(name, new Map());
}
}
private unwrapToken<T>(token: ProviderToken<T>): T {