Skip to content

Commit

Permalink
fix: proper error message while resolving scoped dependency for singl…
Browse files Browse the repository at this point in the history
…eton service
  • Loading branch information
Wroud committed Jun 5, 2024
1 parent 14c8964 commit 7138853
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 30 deletions.
22 changes: 22 additions & 0 deletions packages/di/src/di/ServiceProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,28 @@ describe("ServiceProvider", () => {
"Scoped services require a service scope.",
);
});
it("should not resolve singleton with scoped dependencies", () => {
class Test {}
class Test2 {
constructor(public test: Test) {}
}
ServicesRegistry.register(Test, {
name: "Test",
dependencies: [],
});
ServicesRegistry.register(Test2, {
name: "Test2",
dependencies: [Test],
});
const serviceProvider = new ServiceContainerBuilder()
.addSingleton(Test2)
.addScoped(Test)
.build();
const scope = serviceProvider.createScope();
expect(() => scope.serviceProvider.getService(Test2)).toThrowError(
'Scoped service "Test" cannot be resolved from singleton service.',
);
});
it("should resolve scoped service with singleton dependencies", () => {
class Test {}
class Test2 {
Expand Down
77 changes: 47 additions & 30 deletions packages/di/src/di/ServiceProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export class ServiceProvider implements IServiceProvider {
ServiceType<any>,
IServiceInstanceInfo<any>[]
>;
private currentResolvingLifetime: ServiceLifetime | null;
constructor(
private readonly collection: ServiceCollection,
private readonly parent?: IServiceProvider,
Expand All @@ -28,6 +29,7 @@ export class ServiceProvider implements IServiceProvider {
collection.getDescriptors(IServiceProvider)![0]!,
this,
);
this.currentResolvingLifetime = null;
}

getServices<T>(service: ServiceType<T>): T[] {
Expand Down Expand Up @@ -92,8 +94,21 @@ export class ServiceProvider implements IServiceProvider {
service: ServiceType<T>,
descriptor: IServiceDescriptor<T>,
): T {
const initialize = () => {
try {
const inheritedLifetime = this.currentResolvingLifetime;
try {
if (this.currentResolvingLifetime !== null) {
if (
this.currentResolvingLifetime === ServiceLifetime.Singleton &&
descriptor.lifetime === ServiceLifetime.Scoped
) {
throw new Error(
`Scoped service "${getNameOfServiceType(service)}" cannot be resolved from singleton service.`,
);
}
}
this.currentResolvingLifetime = descriptor.lifetime;

const initialize = () => {
const metadata = ServicesRegistry.get(descriptor.implementation);

if (metadata) {
Expand Down Expand Up @@ -128,36 +143,38 @@ export class ServiceProvider implements IServiceProvider {
}

return descriptor.implementation;
} catch (err: any) {
throw new Error(
`Failed to initiate service "${getNameOfServiceType(service)}":\n\r${err.message}`,
{ cause: err },
);
}
};

switch (descriptor.lifetime) {
case ServiceLifetime.Singleton:
if (this.parent) {
return this.parent.getService(service as any);
} else if (!this.hasInstanceOf(service, descriptor)) {
this.addInstance(service, descriptor, initialize());
}
};

switch (descriptor.lifetime) {
case ServiceLifetime.Singleton:
if (this.parent) {
return this.parent.getService(service as any);
} else if (!this.hasInstanceOf(service, descriptor)) {
this.addInstance(service, descriptor, initialize());
}

return this.getInstanceInfo(service, descriptor)?.instance!;
case ServiceLifetime.Scoped:
if (!this.parent) {
throw new Error("Scoped services require a service scope.");
}
if (!this.hasInstanceOf(service, descriptor)) {
this.addInstance(service, descriptor, initialize());
}
return this.getInstanceInfo(service, descriptor)?.instance!;
case ServiceLifetime.Transient:
return initialize();
return this.getInstanceInfo(service, descriptor)?.instance!;
case ServiceLifetime.Scoped:
if (!this.parent) {
throw new Error("Scoped services require a service scope.");
}
if (!this.hasInstanceOf(service, descriptor)) {
this.addInstance(service, descriptor, initialize());
}
return this.getInstanceInfo(service, descriptor)?.instance!;
case ServiceLifetime.Transient:
return initialize();

default:
throw new Error("Invalid lifetime");
default:
throw new Error("Invalid lifetime");

Check warning on line 169 in packages/di/src/di/ServiceProvider.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 169 in packages/di/src/di/ServiceProvider.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
}
} catch (exception: any) {
throw new Error(
`Failed to initiate service "${getNameOfServiceType(service)}":\n\r${exception.message}`,
{ cause: exception },
);
} finally {

Check warning on line 176 in packages/di/src/di/ServiceProvider.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
this.currentResolvingLifetime = inheritedLifetime;
}
}

Expand Down

1 comment on commit 7138853

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage report

St.
Category Percentage Covered / Total
🟢 Statements 97.62% 534/547
🟢 Branches 95.37% 103/108
🟢 Functions 100% 35/35
🟢 Lines 97.62% 534/547

Test suite run success

86 tests passing in 8 suites.

Report generated by 🧪jest coverage report action from 7138853

Please sign in to comment.