Skip to content

Commit 86094c6

Browse files
Merge pull request #15571 from mag123c/fix/transient-lifecycle-hook
fix(core): skip lifecycle hooks for non-instantiated transient services
2 parents 6e1c4bc + 7f05dd1 commit 86094c6

File tree

3 files changed

+57
-2
lines changed

3 files changed

+57
-2
lines changed

packages/core/injector/injector.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -835,12 +835,14 @@ export class Injector {
835835
: new (metatype as Type<any>)(...instances);
836836

837837
instanceHost.instance = this.instanceDecorator(instanceHost.instance);
838+
instanceHost.isConstructorCalled = true;
838839
} else if (isInContext) {
839840
const factoryReturnValue = (targetMetatype.metatype as any as Function)(
840841
...instances,
841842
);
842843
instanceHost.instance = await factoryReturnValue;
843844
instanceHost.instance = this.instanceDecorator(instanceHost.instance);
845+
instanceHost.isConstructorCalled = true;
844846
}
845847
instanceHost.isResolved = true;
846848
return instanceHost.instance;

packages/core/injector/instance-wrapper.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export interface InstancePerContext<T> {
4444
isResolved?: boolean;
4545
isPending?: boolean;
4646
donePromise?: Promise<unknown>;
47+
isConstructorCalled?: boolean;
4748
}
4849

4950
export interface PropertyMetadata {
@@ -439,7 +440,11 @@ export class InstanceWrapper<T = any> {
439440
const instances = [...this.transientMap.values()];
440441
return iterate(instances)
441442
.map(item => item.get(STATIC_CONTEXT))
442-
.filter(item => !!item)
443+
.filter(item => {
444+
// Only return items where constructor has been actually called
445+
// This prevents calling lifecycle hooks on non-instantiated transient services
446+
return !!(item && item.isConstructorCalled);
447+
})
443448
.toArray();
444449
}
445450

packages/core/test/injector/instance-wrapper.spec.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -896,16 +896,64 @@ describe('InstanceWrapper', () => {
896896
});
897897
});
898898
describe('when instance is transient', () => {
899-
it('should return all static instances', () => {
899+
it('should return instances where constructor was called', () => {
900900
const wrapper = new InstanceWrapper({
901901
scope: Scope.TRANSIENT,
902902
});
903903
const instanceHost = {
904904
instance: {},
905+
isConstructorCalled: true,
905906
};
906907
wrapper.setInstanceByInquirerId(STATIC_CONTEXT, 'test', instanceHost);
907908
expect(wrapper.getStaticTransientInstances()).to.be.eql([instanceHost]);
908909
});
910+
911+
describe('lifecycle hooks on transient services', () => {
912+
// Tests for issue #15553: prevent lifecycle hooks on non-instantiated transient services
913+
it('should filter out instances created with Object.create (prototype only)', () => {
914+
const wrapper = new InstanceWrapper({
915+
scope: Scope.TRANSIENT,
916+
});
917+
// Simulates what happens in cloneTransientInstance
918+
const prototypeOnlyInstance = {
919+
instance: Object.create({}),
920+
isResolved: true, // This is set to true incorrectly in injector.ts
921+
isConstructorCalled: false, // But constructor was never called
922+
};
923+
wrapper.setInstanceByInquirerId(
924+
STATIC_CONTEXT,
925+
'inquirer',
926+
prototypeOnlyInstance,
927+
);
928+
929+
// Should not include this instance for lifecycle hooks
930+
expect(wrapper.getStaticTransientInstances()).to.be.eql([]);
931+
});
932+
933+
it('should include instances where constructor was actually invoked', () => {
934+
class TestService {}
935+
const wrapper = new InstanceWrapper({
936+
scope: Scope.TRANSIENT,
937+
metatype: TestService,
938+
});
939+
// Simulates what happens after instantiateClass
940+
const properInstance = {
941+
instance: new TestService(),
942+
isResolved: true,
943+
isConstructorCalled: true,
944+
};
945+
wrapper.setInstanceByInquirerId(
946+
STATIC_CONTEXT,
947+
'inquirer',
948+
properInstance,
949+
);
950+
951+
// Should include this instance for lifecycle hooks
952+
expect(wrapper.getStaticTransientInstances()).to.be.eql([
953+
properInstance,
954+
]);
955+
});
956+
});
909957
});
910958
});
911959

0 commit comments

Comments
 (0)