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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user