Skip to content

Commit 368691c

Browse files
Youmooclaude
andcommitted
fix(core): prevent injector hang when design:paramtypes is missing
When `emitDecoratorMetadata` is unavailable (e.g. esbuild), `@Inject()` decorators create a sparse array via `SELF_DECLARED_DEPS_METADATA`. `Array.prototype.map` skips sparse holes, so the `Barrier` in `resolveConstructorParams` never receives enough signals and hangs indefinitely. Use `Array.from()` in `reflectConstructorParams` to convert sparse arrays into dense arrays with explicit `undefined` values, ensuring `map` iterates every index and the Barrier resolves correctly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent dea5279 commit 368691c

2 files changed

Lines changed: 79 additions & 1 deletion

File tree

packages/core/injector/injector.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ export class Injector {
444444
const selfParams = this.reflectSelfParams<T>(type);
445445

446446
selfParams.forEach(({ index, param }) => (paramtypes[index] = param));
447-
return paramtypes;
447+
return Array.from(paramtypes);
448448
}
449449

450450
public reflectOptionalParams<T>(type: Type<T>): any[] {

packages/core/test/injector/injector.spec.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,53 @@ describe('Injector', () => {
744744
{ id: 2 },
745745
);
746746
});
747+
748+
it('should not hang when a parameter is missing @Inject and design:paramtypes is absent', async function () {
749+
this.timeout(500);
750+
751+
@Injectable()
752+
class CatService {}
753+
754+
@Injectable()
755+
class DogService {}
756+
757+
@Injectable()
758+
class ZooService {
759+
constructor(
760+
@Inject(CatService) cat: CatService,
761+
dog: DogService, // no @Inject — becomes a sparse array hole without design:paramtypes
762+
@Inject(CatService) cat2: CatService,
763+
) {}
764+
}
765+
766+
// Simulate missing emitDecoratorMetadata (esbuild).
767+
// @Inject writes to SELF_DECLARED_DEPS_METADATA at indices 0 and 2,
768+
// so without design:paramtypes the paramtypes array is sparse: [CatService, <hole>, CatService].
769+
// Before the fix, Array.map skipped the hole, so the Barrier never reached its
770+
// target count and resolveConstructorParams hung forever.
771+
Reflect.deleteMetadata('design:paramtypes', ZooService);
772+
773+
// Register CatService in the module so indices 0 and 2 resolve successfully
774+
// and reach signalAndWait() — this is required to reproduce the hang.
775+
const container = new NestContainer();
776+
const { moduleRef } = (await container.addModule(
777+
class TestModule {},
778+
[],
779+
))!;
780+
moduleRef.addProvider({
781+
provide: CatService,
782+
useClass: CatService,
783+
});
784+
785+
const wrapper = new InstanceWrapper({ metatype: ZooService });
786+
787+
const result = await injector
788+
.resolveConstructorParams(wrapper, moduleRef, undefined, () => {})
789+
.then(() => 'resolved')
790+
.catch(() => 'rejected');
791+
792+
expect(result).to.be.eq('rejected');
793+
});
747794
});
748795

749796
describe('resolveProperties', () => {
@@ -761,6 +808,37 @@ describe('Injector', () => {
761808
});
762809
});
763810

811+
describe('reflectConstructorParams', () => {
812+
it('should not produce sparse arrays when design:paramtypes is missing', () => {
813+
@Injectable()
814+
class CatService {}
815+
816+
@Injectable()
817+
class DogService {}
818+
819+
@Injectable()
820+
class ZooService {
821+
constructor(
822+
@Inject(CatService) cat: CatService,
823+
dog: DogService, // no @Inject
824+
@Inject(CatService) cat2: CatService,
825+
) {}
826+
}
827+
828+
// Simulate missing emitDecoratorMetadata (esbuild)
829+
Reflect.deleteMetadata('design:paramtypes', ZooService);
830+
831+
const injector = new Injector();
832+
const params = injector.reflectConstructorParams(ZooService);
833+
834+
// Should be a dense array — no holes
835+
expect(Object.keys(params)).to.deep.eq(['0', '1', '2']);
836+
// Index 1 should be explicit undefined, not a hole
837+
expect(1 in params).to.be.true;
838+
expect(params[1]).to.be.undefined;
839+
});
840+
});
841+
764842
describe('getClassDependencies', () => {
765843
it('should return an array that consists of deps and optional dep ids', async () => {
766844
class FixtureDep1 {}

0 commit comments

Comments
 (0)