Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions integration/scopes/e2e/transient-scope.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,51 @@ describe('Transient scope', () => {
});
});

describe('when DEFAULT scoped provider has deeply nested TRANSIENT chain', () => {
let app: INestApplication;

@Injectable({ scope: Scope.TRANSIENT })
class DeepNestedTransient {
public static constructorCalled = false;

constructor() {
DeepNestedTransient.constructorCalled = true;
}
}

@Injectable({ scope: Scope.TRANSIENT })
class MiddleTransient {
constructor(public readonly nested: DeepNestedTransient) {}
}

@Injectable()
class RootService {
constructor(public readonly middle: MiddleTransient) {}
}

before(async () => {
DeepNestedTransient.constructorCalled = false;

const module = await Test.createTestingModule({
providers: [RootService, MiddleTransient, DeepNestedTransient],
}).compile();

app = module.createNestApplication();
await app.init();
});

it('should call constructor of deeply nested TRANSIENT provider', () => {
const rootService = app.get(RootService);

expect(DeepNestedTransient.constructorCalled).to.be.true;
expect(rootService.middle.nested).to.be.instanceOf(DeepNestedTransient);
});

after(async () => {
await app.close();
});
});

describe('when nested transient providers are used in request scope', () => {
let server: any;
let app: INestApplication;
Expand Down
33 changes: 26 additions & 7 deletions packages/core/injector/instance-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,17 +408,36 @@ export class InstanceWrapper<T = any> {
contextId: ContextId,
inquirer: InstanceWrapper | undefined,
): boolean {
if (!this.isDependencyTreeStatic() || contextId !== STATIC_CONTEXT) {
return false;
}

// Non-transient provider in static context
if (!this.isTransient) {
return true;
}

const isInquirerRequestScoped =
inquirer && !inquirer.isDependencyTreeStatic();
const isStaticTransient = this.isTransient && !isInquirerRequestScoped;
const rootInquirer = inquirer?.getRootInquirer();
return (
this.isDependencyTreeStatic() &&
contextId === STATIC_CONTEXT &&
(!this.isTransient ||
(isStaticTransient && !!inquirer && !inquirer.isTransient) ||
(isStaticTransient && !!rootInquirer && !rootInquirer.isTransient))
);

// Transient provider inquired by non-transient (e.g., DEFAULT -> TRANSIENT)
if (isStaticTransient && inquirer && !inquirer.isTransient) {
return true;
}

// Nested transient with non-transient root (e.g., DEFAULT -> TRANSIENT -> TRANSIENT)
if (isStaticTransient && rootInquirer && !rootInquirer.isTransient) {
return true;
}

// Nested transient during initial instantiation (rootInquirer not yet set)
if (isStaticTransient && inquirer?.isTransient && !rootInquirer) {
return true;
}

return false;
}

public attachRootInquirer(inquirer: InstanceWrapper) {
Expand Down
79 changes: 79 additions & 0 deletions packages/core/test/injector/nested-transient-isolation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,4 +190,83 @@ describe('Nested Transient Isolation', () => {
);
});
});

describe('when DEFAULT scoped provider depends on nested TRANSIENT chain', () => {
@Injectable({ scope: Scope.TRANSIENT })
class NestedTransientService {
public static constructorCalled = false;
public readonly value = 'nested-initialized';

constructor() {
NestedTransientService.constructorCalled = true;
}
}

@Injectable({ scope: Scope.TRANSIENT })
class TransientService {
public static constructorCalled = false;

constructor(public readonly nested: NestedTransientService) {
TransientService.constructorCalled = true;
}
}

@Injectable()
class DefaultScopedParent {
constructor(public readonly transient: TransientService) {}
}

let nestedTransientWrapper: InstanceWrapper;
let transientWrapper: InstanceWrapper;
let parentWrapper: InstanceWrapper;

beforeEach(() => {
NestedTransientService.constructorCalled = false;
TransientService.constructorCalled = false;

nestedTransientWrapper = new InstanceWrapper({
name: NestedTransientService.name,
token: NestedTransientService,
metatype: NestedTransientService,
scope: Scope.TRANSIENT,
host: module,
});

transientWrapper = new InstanceWrapper({
name: TransientService.name,
token: TransientService,
metatype: TransientService,
scope: Scope.TRANSIENT,
host: module,
});

parentWrapper = new InstanceWrapper({
name: DefaultScopedParent.name,
token: DefaultScopedParent,
metatype: DefaultScopedParent,
scope: Scope.DEFAULT,
host: module,
});

module.providers.set(NestedTransientService, nestedTransientWrapper);
module.providers.set(TransientService, transientWrapper);
module.providers.set(DefaultScopedParent, parentWrapper);
});

it('should instantiate nested TRANSIENT providers from DEFAULT scope', async () => {
await injector.loadInstance(parentWrapper, module.providers, module);

const parentInstance = parentWrapper.instance;

expect(TransientService.constructorCalled).to.be.true;
expect(NestedTransientService.constructorCalled).to.be.true;
expect(parentInstance.transient).to.be.instanceOf(TransientService);
expect(parentInstance.transient.nested).to.be.instanceOf(
NestedTransientService,
);
expect(parentInstance.transient.nested.value).to.equal(
'nested-initialized',
);
});
});
});
Loading