fix(core): fix race condition in class dependency resolution

Fix race condition in class dependency resolution, which could
otherwise lead to undefined or null injection. Split the resolution
process in 2 parts with barrier synchronization in between to ensure all
dependencies are present in dependant's instance wrapper and the
staticity of its dependency tree is evaluated correctly.

Closes #4873
This commit is contained in:
Jiri Hajek
2025-07-14 11:50:05 +02:00
parent 138577e50a
commit a453b6375e
8 changed files with 428 additions and 107 deletions

View File

@@ -0,0 +1,130 @@
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import * as sinon from 'sinon';
import { Global, Inject, Injectable, Module, Scope } from '@nestjs/common';
@Global()
@Module({})
export class GlobalModule1 {}
@Global()
@Module({})
export class GlobalModule2 {}
@Global()
@Module({})
export class GlobalModule3 {}
@Global()
@Module({})
export class GlobalModule4 {}
@Global()
@Module({})
export class GlobalModule5 {}
@Global()
@Module({})
export class GlobalModule6 {}
@Global()
@Module({})
export class GlobalModule7 {}
@Global()
@Module({})
export class GlobalModule8 {}
@Global()
@Module({})
export class GlobalModule9 {}
@Global()
@Module({})
export class GlobalModule10 {}
@Injectable()
class TransientProvider {}
@Injectable()
class RequestProvider {}
@Injectable()
export class Dependant {
constructor(
private readonly transientProvider: TransientProvider,
@Inject(RequestProvider)
private readonly requestProvider: RequestProvider,
) {}
public checkDependencies() {
expect(this.transientProvider).to.be.instanceOf(TransientProvider);
expect(this.requestProvider).to.be.instanceOf(RequestProvider);
}
}
@Global()
@Module({
providers: [
{
provide: TransientProvider,
scope: Scope.TRANSIENT,
useClass: TransientProvider,
},
{
provide: Dependant,
scope: Scope.DEFAULT,
useClass: Dependant,
},
],
})
export class GlobalModuleWithTransientProviderAndDependant {}
@Global()
@Module({
providers: [
{
provide: RequestProvider,
scope: Scope.REQUEST,
useFactory: () => {
return new RequestProvider();
},
},
],
exports: [RequestProvider],
})
export class GlobalModuleWithRequestProvider {}
@Module({
imports: [
GlobalModule1,
GlobalModule2,
GlobalModule3,
GlobalModule4,
GlobalModule5,
GlobalModule6,
GlobalModule7,
GlobalModule8,
GlobalModule9,
GlobalModule10,
GlobalModuleWithTransientProviderAndDependant,
GlobalModuleWithRequestProvider,
],
})
export class AppModule {}
describe('Many global modules', () => {
it('should inject request-scoped useFactory provider and transient-scoped useClass provider from different modules', async () => {
const moduleBuilder = Test.createTestingModule({
imports: [AppModule],
});
const moduleRef = await moduleBuilder.compile();
const dependant = await moduleRef.resolve(Dependant);
const checkDependenciesSpy = sinon.spy(dependant, 'checkDependencies');
dependant.checkDependencies();
expect(checkDependenciesSpy.called).to.be.true;
});
});

View File

@@ -2197,22 +2197,6 @@
},
"id": "1976848738"
},
"-2105726668": {
"source": "-1803759743",
"target": "1010833816",
"metadata": {
"type": "class-to-class",
"sourceModuleName": "PropertiesModule",
"sourceClassName": "PropertiesService",
"targetClassName": "token",
"sourceClassToken": "PropertiesService",
"targetClassToken": "token",
"targetModuleName": "PropertiesModule",
"keyOrIndex": "token",
"injectionType": "property"
},
"id": "-2105726668"
},
"-21463590": {
"source": "-1378706112",
"target": "1004276345",
@@ -2229,6 +2213,22 @@
},
"id": "-21463590"
},
"-2105726668": {
"source": "-1803759743",
"target": "1010833816",
"metadata": {
"type": "class-to-class",
"sourceModuleName": "PropertiesModule",
"sourceClassName": "PropertiesService",
"targetClassName": "token",
"sourceClassToken": "PropertiesService",
"targetClassToken": "token",
"targetModuleName": "PropertiesModule",
"keyOrIndex": "token",
"injectionType": "property"
},
"id": "-2105726668"
},
"-1657371464": {
"source": "-1673986099",
"target": "1919157847",