diff --git a/.vscode/settings.json b/.vscode/settings.json index be4f15f..9a5d465 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,7 +15,7 @@ "explorer.fileNesting.enabled": true, "explorer.fileNesting.expand": false, "explorer.fileNesting.patterns": { - "*.ts": "${capture}.js, ${capture}.test.tsx, ${capture}.test.ts, ${capture}.*.ts, ${capture}.d.ts", + "*.ts": "${capture}.js, ${capture}.test.tsx, ${capture}.test.ts, ${capture}.bench.ts, ${capture}.*.ts, ${capture}.d.ts", "*.d.ts": "${capture}.d.ts.map", "*.js": "${capture}.js.map, ${capture}.min.js, ${capture}.d.ts, ${capture}.d.ts.map", "*.jsx": "${capture}.js, ${capture}.jsx.map", diff --git a/packages/@wroud/di-react/src/useServiceIterator.ts b/packages/@wroud/di-react/src/useServiceIterator.ts index 6a8d176..614987d 100644 --- a/packages/@wroud/di-react/src/useServiceIterator.ts +++ b/packages/@wroud/di-react/src/useServiceIterator.ts @@ -9,7 +9,8 @@ export function useServiceIterator(type: IResolverServiceType): T { const iterator = ServiceProvider.internalGetService( provider, type, - new Set(), + null, + { next: null, value: null }, "async", ); diff --git a/packages/@wroud/di-tools-analyzer/src/ServiceCollectionProxy.ts b/packages/@wroud/di-tools-analyzer/src/ServiceCollectionProxy.ts index 2f29b07..d36f59a 100644 --- a/packages/@wroud/di-tools-analyzer/src/ServiceCollectionProxy.ts +++ b/packages/@wroud/di-tools-analyzer/src/ServiceCollectionProxy.ts @@ -33,7 +33,7 @@ export class ServiceCollectionProxy { }, getDescriptors( service: SingleServiceType, - ): IServiceDescriptor[] { + ): readonly IServiceDescriptor[] { return collection.getDescriptors(service); }, addScoped( diff --git a/packages/@wroud/di-tools-analyzer/src/loadImplementation.ts b/packages/@wroud/di-tools-analyzer/src/loadImplementation.ts index 3715121..91981f8 100644 --- a/packages/@wroud/di-tools-analyzer/src/loadImplementation.ts +++ b/packages/@wroud/di-tools-analyzer/src/loadImplementation.ts @@ -10,7 +10,8 @@ export async function getDeps(descriptor: IServiceDescriptor) { return null as any; }, descriptor, - new Set(), + null, + { next: null, value: null }, "async", ), ); diff --git a/packages/@wroud/di-tools-benchmark/README.md b/packages/@wroud/di-tools-benchmark/README.md new file mode 100644 index 0000000..1eec829 --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/README.md @@ -0,0 +1 @@ +# @wroud/di-tools-benchmark diff --git a/packages/@wroud/di-tools-benchmark/package.json b/packages/@wroud/di-tools-benchmark/package.json new file mode 100644 index 0000000..83b55c4 --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/package.json @@ -0,0 +1,48 @@ +{ + "name": "@wroud/di-tools-benchmark", + "type": "module", + "private": true, + "sideEffects": [], + "exports": { + ".": "./lib/index.js", + "./*": "./lib/*.js" + }, + "scripts": { + "build:legacy-decorators": "tsc --build tsconfig.legacy-decorators.json", + "build:modern": "tsc --build tsconfig.modern.json", + "build:benchmark": "tsc --build tsconfig.json", + "build": "yarn build:modern && yarn build:legacy-decorators && yarn build:benchmark", + "bench": "benchmark run", + "clear": "rimraf lib" + }, + "files": [ + "package.json", + "LICENSE", + "README.md", + "CHANGELOG.md", + "lib", + "!lib/**/*.d.ts.map", + "!lib/**/*.test.js", + "!lib/**/*.test.d.ts", + "!lib/**/*.test.d.ts.map", + "!lib/**/*.test.js.map", + "!lib/tests", + "!.tsbuildinfo" + ], + "packageManager": "yarn@4.5.0", + "devDependencies": { + "@wroud/tsconfig": "workspace:^", + "rimraf": "^6", + "typescript": "^5" + }, + "dependencies": { + "@wroud/di": "workspace:^", + "@wroud/tests-runner": "workspace:^", + "brandi": "^5", + "inversify": "^6", + "reflect-metadata": "^0", + "tslib": "^2", + "tsyringe": "^4", + "vitest": "^2" + } +} diff --git a/packages/@wroud/di-tools-benchmark/src/@wroud/createRegisterGet.ts b/packages/@wroud/di-tools-benchmark/src/@wroud/createRegisterGet.ts new file mode 100644 index 0000000..46a700a --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/@wroud/createRegisterGet.ts @@ -0,0 +1,47 @@ +import { bench } from "vitest"; +import { ServiceContainerBuilder, single } from "@wroud/di"; +import { + createServicesTreeWroudDi, + type ServicePair, +} from "./tests/createServicesTreeWroudDi.js"; + +const singletonServices: ServicePair[] = []; +const rootSingleton = createServicesTreeWroudDi(8, 1, singletonServices)!; + +const transientServices: ServicePair[] = []; +const rootTransient = createServicesTreeWroudDi(8, 1, transientServices)!; + +const scopedServices: ServicePair[] = []; +const rootScoped = createServicesTreeWroudDi(8, 1, scopedServices)!; + +const builder = new ServiceContainerBuilder(); + +for (const { service, impl } of singletonServices) { + builder.addSingleton(service, impl); +} + +for (const { service, impl } of transientServices) { + builder.addTransient(service, impl); +} + +for (const { service, impl } of scopedServices) { + builder.addScoped(service, impl); +} + +const singletonResolver = single(rootSingleton.service); +const transientResolver = single(rootTransient.service); +const scopedResolver = single(rootScoped.service); + +bench( + "[@wroud/di]", + () => { + const serviceProvider = builder.build(); + serviceProvider.getService(singletonResolver); + serviceProvider.getService(transientResolver); + serviceProvider.createScope().serviceProvider.getService(scopedResolver); + }, + { + time: 5000, + warmupTime: 1000, + }, +); diff --git a/packages/@wroud/di-tools-benchmark/src/@wroud/deep_10.ts b/packages/@wroud/di-tools-benchmark/src/@wroud/deep_10.ts new file mode 100644 index 0000000..0240043 --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/@wroud/deep_10.ts @@ -0,0 +1,17 @@ +import { bench } from "vitest"; +import { ServiceContainerBuilder } from "@wroud/di"; +import { createDeepServices } from "./tests/createDeepServices.js"; + +const { lastService, services } = createDeepServices(10); + +const builder = new ServiceContainerBuilder(); + +for (const { service, impl } of services) { + builder.addTransient(service, impl); +} + +const serviceProvider = builder.build(); + +bench("[@wroud/di]", () => { + serviceProvider.getService(lastService); +}); diff --git a/packages/@wroud/di-tools-benchmark/src/@wroud/deep_100.ts b/packages/@wroud/di-tools-benchmark/src/@wroud/deep_100.ts new file mode 100644 index 0000000..12b447d --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/@wroud/deep_100.ts @@ -0,0 +1,17 @@ +import { bench } from "vitest"; +import { ServiceContainerBuilder } from "@wroud/di"; +import { createDeepServices } from "./tests/createDeepServices.js"; + +const { lastService, services } = createDeepServices(100); + +const builder = new ServiceContainerBuilder(); + +for (const { service, impl } of services) { + builder.addTransient(service, impl); +} + +const serviceProvider = builder.build(); + +bench("[@wroud/di]", () => { + serviceProvider.getService(lastService); +}); diff --git a/packages/@wroud/di-tools-benchmark/src/@wroud/flat_10.ts b/packages/@wroud/di-tools-benchmark/src/@wroud/flat_10.ts new file mode 100644 index 0000000..53cd9d3 --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/@wroud/flat_10.ts @@ -0,0 +1,97 @@ +import { bench } from "vitest"; +import { + createService, + injectable, + ServiceContainerBuilder, + single, +} from "@wroud/di"; + +const a0 = createService("A0"); +const a1 = createService("A1"); +const a2 = createService("A2"); +const a3 = createService("A3"); +const a4 = createService("A4"); +const a5 = createService("A5"); +const a6 = createService("A6"); +const a7 = createService("A7"); +const a8 = createService("A8"); +const a9 = createService("A9"); +@injectable(({ single }) => [ + single(a0), + single(a1), + single(a2), + single(a3), + single(a4), + single(a5), + single(a6), + single(a7), + single(a8), +]) +class A9 { + constructor( + a0: any, + a1: any, + a2: any, + a3: any, + a4: any, + a5: any, + a6: any, + a7: any, + a8: any, + ) {} +} + +const builder = new ServiceContainerBuilder() + .addTransient( + a0, + @injectable() + class A0 {}, + ) + .addTransient( + a1, + @injectable() + class A1 {}, + ) + .addTransient( + a2, + @injectable() + class A2 {}, + ) + .addTransient( + a3, + @injectable() + class A3 {}, + ) + .addTransient( + a4, + @injectable() + class A4 {}, + ) + .addTransient( + a5, + @injectable() + class A5 {}, + ) + .addTransient( + a6, + @injectable() + class A6 {}, + ) + .addTransient( + a7, + @injectable() + class A7 {}, + ) + .addTransient( + a8, + @injectable() + class A8 {}, + ) + .addTransient(a9, A9); + +const serviceProvider = builder.build(); + +const a9Resolver = single(a9); +bench("[@wroud/di]", () => { + serviceProvider.getService(a9Resolver); +}); diff --git a/packages/@wroud/di-tools-benchmark/src/@wroud/get_scoped.ts b/packages/@wroud/di-tools-benchmark/src/@wroud/get_scoped.ts new file mode 100644 index 0000000..33e3c63 --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/@wroud/get_scoped.ts @@ -0,0 +1,23 @@ +import { bench } from "vitest"; +import { + createService, + injectable, + ServiceContainerBuilder, + single, +} from "@wroud/di"; + +const serviceA = createService("serviceA"); +@injectable() +class implA {} + +const scopedProvider = new ServiceContainerBuilder() + .addScoped(serviceA, implA) + .build() + .createScope().serviceProvider; +scopedProvider.getService(serviceA); + +const serviceAResolver = single(serviceA); + +bench("[@wroud/di]", () => { + scopedProvider.getService(serviceAResolver); +}); diff --git a/packages/@wroud/di-tools-benchmark/src/@wroud/get_singleton.ts b/packages/@wroud/di-tools-benchmark/src/@wroud/get_singleton.ts new file mode 100644 index 0000000..787eb7f --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/@wroud/get_singleton.ts @@ -0,0 +1,22 @@ +import { bench } from "vitest"; +import { + createService, + injectable, + ServiceContainerBuilder, + single, +} from "@wroud/di"; + +const serviceA = createService("serviceA"); +@injectable() +class implA {} + +const singletonProvider = new ServiceContainerBuilder() + .addSingleton(serviceA, implA) + .build(); + +singletonProvider.getService(serviceA); + +const serviceAResolver = single(serviceA); +bench("[@wroud/di]", () => { + singletonProvider.getService(serviceAResolver); +}); diff --git a/packages/@wroud/di-tools-benchmark/src/@wroud/get_transient.ts b/packages/@wroud/di-tools-benchmark/src/@wroud/get_transient.ts new file mode 100644 index 0000000..31535f9 --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/@wroud/get_transient.ts @@ -0,0 +1,20 @@ +import { bench } from "vitest"; +import { + createService, + injectable, + ServiceContainerBuilder, + single, +} from "@wroud/di"; + +const serviceA = createService("serviceA"); +@injectable() +class implA {} + +const serviceProvider = new ServiceContainerBuilder() + .addTransient(serviceA, implA) + .build(); + +const serviceAResolver = single(serviceA); +bench("[@wroud/di]", () => { + serviceProvider.getService(serviceAResolver); +}); diff --git a/packages/@wroud/di-tools-benchmark/src/@wroud/register.ts b/packages/@wroud/di-tools-benchmark/src/@wroud/register.ts new file mode 100644 index 0000000..004ca52 --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/@wroud/register.ts @@ -0,0 +1,21 @@ +import { bench } from "vitest"; +import { createService, injectable, ServiceContainerBuilder } from "@wroud/di"; + +const service = createService("service"); +@injectable() +class impl {} + +bench("[@wroud/di] singleton", () => { + const builder = new ServiceContainerBuilder(); + builder.addSingleton(service, impl); +}); + +bench("[@wroud/di] transient", () => { + const builder = new ServiceContainerBuilder(); + builder.addTransient(service, impl); +}); + +bench("[@wroud/di] scoped", () => { + const builder = new ServiceContainerBuilder(); + builder.addScoped(service, impl); +}); diff --git a/packages/@wroud/di-tools-benchmark/src/@wroud/register_1000.ts b/packages/@wroud/di-tools-benchmark/src/@wroud/register_1000.ts new file mode 100644 index 0000000..846fb4d --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/@wroud/register_1000.ts @@ -0,0 +1,33 @@ +import { bench } from "vitest"; +import { createService, injectable, ServiceContainerBuilder } from "@wroud/di"; + +const services: { service: any; impl: any }[] = []; + +for (let i = 0; i < 1000; i++) { + @injectable() + class impl {} + const service = createService("service"); + + services.push({ service, impl }); +} + +bench("[@wroud/di] singleton", () => { + const builder = new ServiceContainerBuilder(); + for (const { service, impl } of services) { + builder.addSingleton(service, impl); + } +}); + +bench("[@wroud/di] transient", () => { + const builder = new ServiceContainerBuilder(); + for (const { service, impl } of services) { + builder.addTransient(service, impl); + } +}); + +bench("[@wroud/di] scoped", () => { + const builder = new ServiceContainerBuilder(); + for (const { service, impl } of services) { + builder.addScoped(service, impl); + } +}); diff --git a/packages/@wroud/di-tools-benchmark/src/@wroud/tests/IServicePair.ts b/packages/@wroud/di-tools-benchmark/src/@wroud/tests/IServicePair.ts new file mode 100644 index 0000000..5ea2688 --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/@wroud/tests/IServicePair.ts @@ -0,0 +1,6 @@ +import type { IServiceConstructor, SingleServiceType } from "@wroud/di/types"; + +export interface IServicePair { + service: SingleServiceType; + impl: IServiceConstructor; +} diff --git a/packages/@wroud/di-tools-benchmark/src/@wroud/tests/createAsyncMockedService.ts b/packages/@wroud/di-tools-benchmark/src/@wroud/tests/createAsyncMockedService.ts new file mode 100644 index 0000000..8de3119 --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/@wroud/tests/createAsyncMockedService.ts @@ -0,0 +1,19 @@ +import { injectable } from "@wroud/di"; + +export function createAsyncMockedService( + name: string, + deps: () => any[] = () => [], + constructorImplementation?: (...deps: any[]) => void, +) { + @injectable(deps) + class Disposable { + readonly deps: any[]; + constructor(...deps: any[]) { + this.deps = deps; + constructorImplementation?.(...deps); + } + } + + Object.defineProperty(Disposable, "name", { value: name }); + return Disposable; +} diff --git a/packages/@wroud/di-tools-benchmark/src/@wroud/tests/createDeepServices.ts b/packages/@wroud/di-tools-benchmark/src/@wroud/tests/createDeepServices.ts new file mode 100644 index 0000000..2d4d8d8 --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/@wroud/tests/createDeepServices.ts @@ -0,0 +1,27 @@ +import { createService, injectable, single } from "@wroud/di"; +import type { ServiceType, SingleServiceType } from "@wroud/di/types"; +import type { IServicePair } from "./IServicePair.js"; + +export function createDeepServices(deep: number): { + lastService: ServiceType; + services: readonly IServicePair[]; +} { + const services: IServicePair[] = []; + + let lastService: SingleServiceType | null = null; + + for (let i = 0; i < deep; i++) { + const service = createService(`service${i}`); + + //@ts-ignore + @injectable(lastService ? () => [single(lastService)] : undefined) + class impl { + constructor(service: any) {} + } + + lastService = service; + services.push({ service, impl }); + } + + return { lastService: single(lastService!), services }; +} diff --git a/packages/@wroud/di-tools-benchmark/src/@wroud/tests/createServicesTreeWroudDi.ts b/packages/@wroud/di-tools-benchmark/src/@wroud/tests/createServicesTreeWroudDi.ts new file mode 100644 index 0000000..cd6e998 --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/@wroud/tests/createServicesTreeWroudDi.ts @@ -0,0 +1,31 @@ +import type { IServiceConstructor, SingleServiceType } from "@wroud/di/types"; +import { + createServicesTree, + type IServicePair, +} from "../../tools/createServicesTree.js"; +import { createService, injectable, single } from "@wroud/di"; + +export type ServicePair = IServicePair< + SingleServiceType, + IServiceConstructor +>; +export function createServicesTreeWroudDi( + deep: number, + level: number, + services: ServicePair[], +): ServicePair | undefined { + return createServicesTree( + deep, + level, + services, + (i) => createService(`service${i}`), + (i, deps) => { + @injectable(() => deps.map(single)) + class Impl { + constructor(...service: any[]) {} + } + + return Impl; + }, + ); +} diff --git a/packages/@wroud/di-tools-benchmark/src/@wroud/tests/createSyncMockedService.ts b/packages/@wroud/di-tools-benchmark/src/@wroud/tests/createSyncMockedService.ts new file mode 100644 index 0000000..cfcd31b --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/@wroud/tests/createSyncMockedService.ts @@ -0,0 +1,19 @@ +import { injectable } from "@wroud/di"; + +export function createSyncMockedService( + name: string, + deps: () => any[] = () => [], + constructorImplementation?: (...deps: any[]) => void, +) { + @injectable(deps) + class Disposable { + readonly deps: any[]; + constructor(...deps: any[]) { + this.deps = deps; + constructorImplementation?.(...deps); + } + } + + Object.defineProperty(Disposable, "name", { value: name }); + return Disposable; +} diff --git a/packages/@wroud/di-tools-benchmark/src/benchmark/di.bench.ts b/packages/@wroud/di-tools-benchmark/src/benchmark/di.bench.ts new file mode 100644 index 0000000..ce0924d --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/benchmark/di.bench.ts @@ -0,0 +1,60 @@ +/// +import { describe } from "vitest"; + +describe("create + register + get", async () => { + await import("@wroud/di-tools-benchmark/@wroud/createRegisterGet"); + await import("@wroud/di-tools-benchmark/tsyringe/createRegisterGet"); + await import("@wroud/di-tools-benchmark/brandi/createRegisterGet"); + await import("@wroud/di-tools-benchmark/inversify/createRegisterGet"); +}); + +describe("get singleton", async () => { + await import("@wroud/di-tools-benchmark/@wroud/get_singleton"); + await import("@wroud/di-tools-benchmark/tsyringe/get_singleton"); + await import("@wroud/di-tools-benchmark/brandi/get_singleton"); + await import("@wroud/di-tools-benchmark/inversify/get_singleton"); +}); + +describe("get scoped", async () => { + await import("@wroud/di-tools-benchmark/@wroud/get_scoped"); + await import("@wroud/di-tools-benchmark/tsyringe/get_scoped"); + await import("@wroud/di-tools-benchmark/brandi/get_scoped"); +}); + +describe("get transient", async () => { + await import("@wroud/di-tools-benchmark/@wroud/get_transient"); + await import("@wroud/di-tools-benchmark/tsyringe/get_transient"); + await import("@wroud/di-tools-benchmark/brandi/get_transient"); + await import("@wroud/di-tools-benchmark/inversify/get_transient"); +}); + +describe("get flat N=10", async () => { + await import("@wroud/di-tools-benchmark/@wroud/flat_10"); + await import("@wroud/di-tools-benchmark/tsyringe/flat_10"); + await import("@wroud/di-tools-benchmark/inversify/flat_10"); +}); + +describe("get deep N=10", async () => { + await import("@wroud/di-tools-benchmark/@wroud/deep_10"); + await import("@wroud/di-tools-benchmark/tsyringe/deep_10"); + await import("@wroud/di-tools-benchmark/inversify/deep_10"); +}); + +describe("get deep N=100", async () => { + await import("@wroud/di-tools-benchmark/@wroud/deep_100"); + await import("@wroud/di-tools-benchmark/tsyringe/deep_100"); + await import("@wroud/di-tools-benchmark/inversify/deep_100"); +}); + +describe("register", async () => { + await import("@wroud/di-tools-benchmark/@wroud/register"); + await import("@wroud/di-tools-benchmark/tsyringe/register"); + await import("@wroud/di-tools-benchmark/brandi/register"); + await import("@wroud/di-tools-benchmark/inversify/register"); +}); + +describe("register N=1000", async () => { + await import("@wroud/di-tools-benchmark/@wroud/register_1000"); + await import("@wroud/di-tools-benchmark/tsyringe/register_1000"); + await import("@wroud/di-tools-benchmark/inversify/register_1000"); +}); diff --git a/packages/@wroud/di-tools-benchmark/src/brandi/createRegisterGet.ts b/packages/@wroud/di-tools-benchmark/src/brandi/createRegisterGet.ts new file mode 100644 index 0000000..b4a3671 --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/brandi/createRegisterGet.ts @@ -0,0 +1,39 @@ +import { bench } from "vitest"; +import { Container } from "brandi"; +import { + createServicesTreeBrandi, + type ServicePair, +} from "./tools/createServicesTreeBrandi.js"; + +const singletonServices: ServicePair[] = []; +const rootSingleton = createServicesTreeBrandi(8, 1, singletonServices)!; + +const transientServices: ServicePair[] = []; +const rootTransient = createServicesTreeBrandi(8, 1, transientServices)!; + +const scopedServices: ServicePair[] = []; +const rootScoped = createServicesTreeBrandi(8, 1, scopedServices)!; + +bench( + "[brandi]", + () => { + const myContainer = new Container(); + for (const { service, impl } of singletonServices) { + myContainer.bind(service).toInstance(impl).inSingletonScope(); + } + for (const { service, impl } of transientServices) { + myContainer.bind(service).toInstance(impl).inTransientScope(); + } + for (const { service, impl } of scopedServices) { + myContainer.bind(service).toInstance(impl).inContainerScope(); + } + + myContainer.get(rootSingleton.service); + myContainer.get(rootTransient.service); + new Container().extend(myContainer).get(rootScoped.service); + }, + { + time: 5000, + warmupTime: 1000, + }, +); diff --git a/packages/@wroud/di-tools-benchmark/src/brandi/get_scoped.ts b/packages/@wroud/di-tools-benchmark/src/brandi/get_scoped.ts new file mode 100644 index 0000000..b1491de --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/brandi/get_scoped.ts @@ -0,0 +1,14 @@ +import { bench } from "vitest"; +import { Container, token } from "brandi"; + +class A {} +const scoped = token("serviceA"); + +const singletonContainer = new Container(); +singletonContainer.bind(scoped).toInstance(A).inContainerScope(); + +const scopedContainer = new Container().extend(singletonContainer); +scopedContainer.get(scoped); +bench("[brandi]", () => { + scopedContainer.get(scoped); +}); diff --git a/packages/@wroud/di-tools-benchmark/src/brandi/get_singleton.ts b/packages/@wroud/di-tools-benchmark/src/brandi/get_singleton.ts new file mode 100644 index 0000000..f658b0c --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/brandi/get_singleton.ts @@ -0,0 +1,13 @@ +import { bench } from "vitest"; +import { Container, token } from "brandi"; + +class A {} +const singleton = token("serviceA"); + +const singletonContainer = new Container(); +singletonContainer.bind(singleton).toInstance(A).inSingletonScope(); +singletonContainer.get(singleton); + +bench("[brandi]", () => { + singletonContainer.get(singleton); +}); diff --git a/packages/@wroud/di-tools-benchmark/src/brandi/get_transient.ts b/packages/@wroud/di-tools-benchmark/src/brandi/get_transient.ts new file mode 100644 index 0000000..11bab7e --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/brandi/get_transient.ts @@ -0,0 +1,13 @@ +import "reflect-metadata"; +import { bench } from "vitest"; +import { Container, token } from "brandi"; + +class A {} +const transient = token("serviceA"); + +const container = new Container(); +container.bind(transient).toInstance(A).inTransientScope(); + +bench("[brandi]", () => { + container.get(transient); +}); diff --git a/packages/@wroud/di-tools-benchmark/src/brandi/register.ts b/packages/@wroud/di-tools-benchmark/src/brandi/register.ts new file mode 100644 index 0000000..7b88bb6 --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/brandi/register.ts @@ -0,0 +1,21 @@ +import "reflect-metadata"; +import { bench } from "vitest"; +import { Container, token } from "brandi"; + +class A {} +const serviceASymbol = token("serviceA"); + +bench("[brandi] singleton", () => { + const singletonContainer = new Container(); + singletonContainer.bind(serviceASymbol).toInstance(A).inSingletonScope(); +}); + +bench("[brandi] transient", () => { + const singletonContainer = new Container(); + singletonContainer.bind(serviceASymbol).toInstance(A).inTransientScope(); +}); + +bench("[brandi] scoped", () => { + const singletonContainer = new Container(); + singletonContainer.bind(serviceASymbol).toInstance(A).inContainerScope(); +}); diff --git a/packages/@wroud/di-tools-benchmark/src/brandi/tools/createServicesTreeBrandi.ts b/packages/@wroud/di-tools-benchmark/src/brandi/tools/createServicesTreeBrandi.ts new file mode 100644 index 0000000..ec9ea89 --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/brandi/tools/createServicesTreeBrandi.ts @@ -0,0 +1,30 @@ +import { + createServicesTree, + type IServicePair, +} from "../../tools/createServicesTree.js"; +import { injected, token, type Token } from "brandi"; + +export type ServicePair = IServicePair< + Token, + new (...args: any[]) => any +>; +export function createServicesTreeBrandi( + deep: number, + level: number, + services: ServicePair[], +): ServicePair | undefined { + return createServicesTree( + deep, + level, + services, + (i) => token(`service${i}`), + (i, deps) => { + class Impl { + constructor(...service: any[]) {} + } + injected(Impl, ...(deps as any)); + + return Impl; + }, + ); +} diff --git a/packages/@wroud/di-tools-benchmark/src/inversify/createRegisterGet.ts b/packages/@wroud/di-tools-benchmark/src/inversify/createRegisterGet.ts new file mode 100644 index 0000000..9613804 --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/inversify/createRegisterGet.ts @@ -0,0 +1,40 @@ +import "reflect-metadata"; +import { bench } from "vitest"; +import { Container } from "inversify"; +import { + type ServicePair, + createServicesTreeInversify, +} from "./tests/createServicesTreeInversify.js"; + +const singletonServices: ServicePair[] = []; +const rootSingleton = createServicesTreeInversify(8, 1, singletonServices)!; + +const transientServices: ServicePair[] = []; +const rootTransient = createServicesTreeInversify(8, 1, transientServices)!; + +const scopedServices: ServicePair[] = []; +const rootScoped = createServicesTreeInversify(8, 1, scopedServices)!; + +bench( + "[inversify]", + () => { + const myContainer = new Container(); + for (const { service, impl } of singletonServices) { + myContainer.bind(service).to(impl).inSingletonScope(); + } + for (const { service, impl } of transientServices) { + myContainer.bind(service).to(impl).inTransientScope(); + } + for (const { service, impl } of scopedServices) { + myContainer.bind(service).to(impl).inRequestScope(); + } + + myContainer.get(rootSingleton.service); + myContainer.get(rootTransient.service); + myContainer.get(rootScoped.service); + }, + { + time: 5000, + warmupTime: 1000, + }, +); diff --git a/packages/@wroud/di-tools-benchmark/src/inversify/deep_10.ts b/packages/@wroud/di-tools-benchmark/src/inversify/deep_10.ts new file mode 100644 index 0000000..4081f29 --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/inversify/deep_10.ts @@ -0,0 +1,15 @@ +import "reflect-metadata"; +import { bench } from "vitest"; +import { Container } from "inversify"; +import { createDeepServices } from "./tests/createDeepServices.js"; + +const { lastService, services } = createDeepServices(10); + +const container = new Container(); +for (const { service, impl } of services) { + container.bind(service).to(impl).inTransientScope(); +} + +bench("[inversify]", () => { + container.get(lastService); +}); diff --git a/packages/@wroud/di-tools-benchmark/src/inversify/deep_100.ts b/packages/@wroud/di-tools-benchmark/src/inversify/deep_100.ts new file mode 100644 index 0000000..5af7dae --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/inversify/deep_100.ts @@ -0,0 +1,15 @@ +import "reflect-metadata"; +import { bench } from "vitest"; +import { Container } from "inversify"; +import { createDeepServices } from "./tests/createDeepServices.js"; + +const { lastService, services } = createDeepServices(100); + +const container = new Container(); +for (const { service, impl } of services) { + container.bind(service).to(impl).inTransientScope(); +} + +bench("[inversify]", () => { + container.get(lastService); +}); diff --git a/packages/@wroud/di-tools-benchmark/src/inversify/flat_10.ts b/packages/@wroud/di-tools-benchmark/src/inversify/flat_10.ts new file mode 100644 index 0000000..d47e1db --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/inversify/flat_10.ts @@ -0,0 +1,69 @@ +import "reflect-metadata"; +import { bench } from "vitest"; +import { Container, inject, injectable } from "inversify"; + +const a0 = Symbol("A0"); +@injectable() +class A0 {} +const a1 = Symbol("A1"); +@injectable() +class A1 {} +const a2 = Symbol("A2"); +@injectable() +class A2 {} +const a3 = Symbol("A3"); +@injectable() +class A3 {} +const a4 = Symbol("A4"); +@injectable() +class A4 {} +const a5 = Symbol("A5"); +@injectable() +class A5 {} +const a6 = Symbol("A6"); +@injectable() +class A6 {} +const a7 = Symbol("A7"); +@injectable() +class A7 {} +const a8 = Symbol("A8"); +@injectable() +class A8 {} +@injectable() +class A9 { + //@ts-ignore + @inject(a0) private a0: any; + //@ts-ignore + @inject(a1) private a1: any; + //@ts-ignore + @inject(a2) private a2: any; + //@ts-ignore + @inject(a3) private a3: any; + //@ts-ignore + @inject(a4) private a4: any; + //@ts-ignore + @inject(a5) private a5: any; + //@ts-ignore + @inject(a6) private a6: any; + //@ts-ignore + @inject(a7) private a7: any; + //@ts-ignore + @inject(a8) private a8: any; +} +const a9 = Symbol("A9"); + +const container = new Container(); +container.bind(a0).to(A0).inTransientScope(); +container.bind(a1).to(A1).inTransientScope(); +container.bind(a2).to(A2).inTransientScope(); +container.bind(a3).to(A3).inTransientScope(); +container.bind(a4).to(A4).inTransientScope(); +container.bind(a5).to(A5).inTransientScope(); +container.bind(a6).to(A6).inTransientScope(); +container.bind(a7).to(A7).inTransientScope(); +container.bind(a8).to(A8).inTransientScope(); +container.bind(a9).to(A9).inTransientScope(); + +bench("[inversify]", () => { + container.get(a9); +}); diff --git a/packages/@wroud/di-tools-benchmark/src/inversify/get_singleton.ts b/packages/@wroud/di-tools-benchmark/src/inversify/get_singleton.ts new file mode 100644 index 0000000..23d48c4 --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/inversify/get_singleton.ts @@ -0,0 +1,15 @@ +import "reflect-metadata"; +import { bench } from "vitest"; +import { Container, injectable } from "inversify"; + +@injectable() +class A {} +const serviceASymbol = Symbol("serviceA"); + +const singletonContainer = new Container(); +singletonContainer.bind(serviceASymbol).to(A).inSingletonScope(); +singletonContainer.get(serviceASymbol); + +bench("[inversify]", () => { + singletonContainer.get(serviceASymbol); +}); diff --git a/packages/@wroud/di-tools-benchmark/src/inversify/get_transient.ts b/packages/@wroud/di-tools-benchmark/src/inversify/get_transient.ts new file mode 100644 index 0000000..c7ca8ed --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/inversify/get_transient.ts @@ -0,0 +1,14 @@ +import "reflect-metadata"; +import { bench } from "vitest"; +import { Container, injectable } from "inversify"; + +@injectable() +class A {} +const serviceASymbol = Symbol("serviceA"); + +const container = new Container(); +container.bind(serviceASymbol).to(A).inTransientScope(); + +bench("[inversify]", () => { + container.get(serviceASymbol); +}); diff --git a/packages/@wroud/di-tools-benchmark/src/inversify/register.ts b/packages/@wroud/di-tools-benchmark/src/inversify/register.ts new file mode 100644 index 0000000..e874e64 --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/inversify/register.ts @@ -0,0 +1,22 @@ +import "reflect-metadata"; +import { bench } from "vitest"; +import { Container, injectable } from "inversify"; + +@injectable() +class A {} +const serviceASymbol = Symbol("serviceA"); + +bench("[inversify] singleton", () => { + const singletonContainer = new Container(); + singletonContainer.bind(serviceASymbol).to(A).inSingletonScope(); +}); + +bench("[inversify] transient", () => { + const singletonContainer = new Container(); + singletonContainer.bind(serviceASymbol).to(A).inTransientScope(); +}); + +bench("[inversify] scoped", () => { + const singletonContainer = new Container(); + singletonContainer.bind(serviceASymbol).to(A).inRequestScope(); +}); diff --git a/packages/@wroud/di-tools-benchmark/src/inversify/register_1000.ts b/packages/@wroud/di-tools-benchmark/src/inversify/register_1000.ts new file mode 100644 index 0000000..01eebaf --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/inversify/register_1000.ts @@ -0,0 +1,35 @@ +import "reflect-metadata"; +import { bench } from "vitest"; +import { Container, injectable } from "inversify"; + +const services: { service: any; impl: any }[] = []; + +for (let i = 0; i < 1000; i++) { + @injectable() + class impl {} + const service = Symbol("serviceA"); + + services.push({ service, impl }); +} + +bench("[inversify] singleton", () => { + const singletonContainer = new Container(); + + for (const { service, impl } of services) { + singletonContainer.bind(service).to(impl).inSingletonScope(); + } +}); + +bench("[inversify] transient", () => { + const singletonContainer = new Container(); + for (const { service, impl } of services) { + singletonContainer.bind(service).to(impl).inTransientScope(); + } +}); + +bench("[inversify] scoped", () => { + const singletonContainer = new Container(); + for (const { service, impl } of services) { + singletonContainer.bind(service).to(impl).inRequestScope(); + } +}); diff --git a/packages/@wroud/di-tools-benchmark/src/inversify/tests/createDeepServices.ts b/packages/@wroud/di-tools-benchmark/src/inversify/tests/createDeepServices.ts new file mode 100644 index 0000000..e82ccf4 --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/inversify/tests/createDeepServices.ts @@ -0,0 +1,37 @@ +import { inject, injectable } from "inversify"; + +interface IServicePair { + service: symbol; + impl: new (...args: any[]) => any; +} + +export function createDeepServices(deep: number): { + lastService: symbol; + services: readonly IServicePair[]; +} { + const services: IServicePair[] = []; + + let lastService: symbol | null = null; + + for (let i = 0; i < deep; i++) { + const service = Symbol(`service${i}`); + let impl; + if (!lastService) { + @injectable() + class implementation {} + impl = implementation; + } else { + @injectable() + class implementation { + // @ts-ignore + @inject(lastService) private service: any; + } + impl = implementation; + } + + lastService = service; + services.push({ service, impl }); + } + + return { lastService: lastService!, services }; +} diff --git a/packages/@wroud/di-tools-benchmark/src/inversify/tests/createServicesTreeInversify.ts b/packages/@wroud/di-tools-benchmark/src/inversify/tests/createServicesTreeInversify.ts new file mode 100644 index 0000000..5a87901 --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/inversify/tests/createServicesTreeInversify.ts @@ -0,0 +1,30 @@ +import { + createServicesTree, + type IServicePair, +} from "@wroud/di-tools-benchmark/tools/createServicesTree"; +import { decorate, injectable, inject } from "inversify"; + +export type ServicePair = IServicePair any>; +export function createServicesTreeInversify( + deep: number, + level: number, + services: ServicePair[], +): ServicePair | undefined { + return createServicesTree( + deep, + level, + services, + (i) => Symbol(`service${i}`), + (level, deps) => { + class Impl { + constructor(...service: any[]) {} + } + + decorate(injectable(), Impl); + for (let i = 0; i < deps.length; i++) { + decorate(inject(deps[i]!), Impl, i); + } + return Impl; + }, + ); +} diff --git a/packages/@wroud/di-tools-benchmark/src/tools/createServicesTree.ts b/packages/@wroud/di-tools-benchmark/src/tools/createServicesTree.ts new file mode 100644 index 0000000..b96c22d --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/tools/createServicesTree.ts @@ -0,0 +1,42 @@ +export interface IServicePair { + service: TService; + impl: TImplementation; +} + +export function createServicesTree( + deep: number, + level: number, + services: IServicePair[], + getService: (i: number) => TService, + getImplementation: (i: number, deps: TService[]) => TImplementation, +): IServicePair | undefined { + if (level === deep) { + return; + } + + const deps: IServicePair[] = []; + + for (let i = 0; i < level + 1; i++) { + const dep = createServicesTree( + deep, + level + 1, + services, + getService, + getImplementation, + ); + if (dep) { + deps.push(dep); + } + } + + const service = { + service: getService(level), + impl: getImplementation( + level, + deps.map((d) => d.service), + ), + }; + services.push(service); + + return service; +} diff --git a/packages/@wroud/di-tools-benchmark/src/tsyringe/createRegisterGet.ts b/packages/@wroud/di-tools-benchmark/src/tsyringe/createRegisterGet.ts new file mode 100644 index 0000000..17e903b --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/tsyringe/createRegisterGet.ts @@ -0,0 +1,55 @@ +import "reflect-metadata"; +import { bench } from "vitest"; +import { container, Lifecycle } from "tsyringe"; +import { + createServicesTreeTsyringe, + type ServicePair, +} from "./tests/createServicesTreeTsyringe.js"; + +const singletonServices: ServicePair[] = []; +const rootSingleton = createServicesTreeTsyringe(8, 1, singletonServices)!; + +const transientServices: ServicePair[] = []; +const rootTransient = createServicesTreeTsyringe(8, 1, transientServices)!; + +const scopedServices: ServicePair[] = []; +const rootScoped = createServicesTreeTsyringe(8, 1, scopedServices)!; + +bench( + "[tsyringe]", + () => { + const cont = container.createChildContainer(); + + for (const { service, impl } of singletonServices) { + cont.register( + service, + { useClass: impl }, + { lifecycle: Lifecycle.Singleton }, + ); + } + + for (const { service, impl } of transientServices) { + cont.register( + service, + { useClass: impl }, + { lifecycle: Lifecycle.Transient }, + ); + } + + for (const { service, impl } of scopedServices) { + cont.register( + service, + { useClass: impl }, + { lifecycle: Lifecycle.ContainerScoped }, + ); + } + + cont.resolve(rootSingleton.service); + cont.resolve(rootTransient.service); + cont.createChildContainer().resolve(rootScoped.service); + }, + { + time: 5000, + warmupTime: 1000, + }, +); diff --git a/packages/@wroud/di-tools-benchmark/src/tsyringe/deep_10.ts b/packages/@wroud/di-tools-benchmark/src/tsyringe/deep_10.ts new file mode 100644 index 0000000..b4ee5aa --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/tsyringe/deep_10.ts @@ -0,0 +1,18 @@ +import { bench } from "vitest"; +import "reflect-metadata"; +import { container, Lifecycle } from "tsyringe"; +import { createDeepServices } from "./tests/createDeepServices.js"; + +const { lastService, services } = createDeepServices(10); + +for (const { service, impl } of services) { + container.register( + service, + { useClass: impl }, + { lifecycle: Lifecycle.Transient }, + ); +} + +bench("[tsyringe]", () => { + container.resolve(lastService); +}); diff --git a/packages/@wroud/di-tools-benchmark/src/tsyringe/deep_100.ts b/packages/@wroud/di-tools-benchmark/src/tsyringe/deep_100.ts new file mode 100644 index 0000000..f721639 --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/tsyringe/deep_100.ts @@ -0,0 +1,18 @@ +import { bench } from "vitest"; +import "reflect-metadata"; +import { container, Lifecycle } from "tsyringe"; +import { createDeepServices } from "./tests/createDeepServices.js"; + +const { lastService, services } = createDeepServices(100); + +for (const { service, impl } of services) { + container.register( + service, + { useClass: impl }, + { lifecycle: Lifecycle.Transient }, + ); +} + +bench("[tsyringe]", () => { + container.resolve(lastService); +}); diff --git a/packages/@wroud/di-tools-benchmark/src/tsyringe/flat_10.ts b/packages/@wroud/di-tools-benchmark/src/tsyringe/flat_10.ts new file mode 100644 index 0000000..3ef2bb1 --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/tsyringe/flat_10.ts @@ -0,0 +1,131 @@ +import { bench } from "vitest"; +import "reflect-metadata"; +// @ts-ignore +import { container, inject, injectable, Lifecycle } from "tsyringe"; + +const a0 = "A0"; +@injectable() +class A0 {} +const a1 = "A1"; +@injectable() +class A1 {} +const a2 = "A2"; +@injectable() +class A2 {} +const a3 = "A3"; +@injectable() +class A3 {} +const a4 = "A4"; +@injectable() +class A4 {} +const a5 = "A5"; +@injectable() +class A5 {} +const a6 = "A6"; +@injectable() +class A6 {} +const a7 = "A7"; +@injectable() +class A7 {} +const a8 = "A8"; +@injectable() +class A8 {} +const a9 = "A9"; +@injectable() +class A9 { + constructor( + // @ts-ignore + @inject(a0) a0: any, + // @ts-ignore + @inject(a1) a1: any, + // @ts-ignore + @inject(a2) a2: any, + // @ts-ignore + @inject(a3) a3: any, + // @ts-ignore + @inject(a4) a4: any, + // @ts-ignore + @inject(a5) a5: any, + // @ts-ignore + @inject(a6) a6: any, + // @ts-ignore + @inject(a7) a7: any, + // @ts-ignore + @inject(a8) a8: any, + ) {} +} + +container.register( + a0, + { + useClass: A0, + }, + { lifecycle: Lifecycle.Transient }, +); +container.register( + a1, + { + useClass: A1, + }, + { lifecycle: Lifecycle.Transient }, +); +container.register( + a2, + { + useClass: A2, + }, + { lifecycle: Lifecycle.Transient }, +); +container.register( + a3, + { + useClass: A3, + }, + { lifecycle: Lifecycle.Transient }, +); +container.register( + a4, + { + useClass: A4, + }, + { lifecycle: Lifecycle.Transient }, +); +container.register( + a5, + { + useClass: A5, + }, + { lifecycle: Lifecycle.Transient }, +); +container.register( + a6, + { + useClass: A6, + }, + { lifecycle: Lifecycle.Transient }, +); +container.register( + a7, + { + useClass: A7, + }, + { lifecycle: Lifecycle.Transient }, +); +container.register( + a8, + { + useClass: A8, + }, + { lifecycle: Lifecycle.Transient }, +); +container.register( + a9, + { + useClass: A9, + }, + { lifecycle: Lifecycle.Transient }, +); + +bench("[tsyringe]", () => { + container.resolve(A9); +}); diff --git a/packages/@wroud/di-tools-benchmark/src/tsyringe/get_scoped.ts b/packages/@wroud/di-tools-benchmark/src/tsyringe/get_scoped.ts new file mode 100644 index 0000000..49505a4 --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/tsyringe/get_scoped.ts @@ -0,0 +1,13 @@ +import { bench } from "vitest"; +import "reflect-metadata"; +import { container, scoped, Lifecycle, injectable } from "tsyringe"; + +@scoped(Lifecycle.ContainerScoped) +@injectable() +class Scoped {} + +const scopedContainer = container.createChildContainer(); +scopedContainer.resolve(Scoped); +bench("[tsyringe]", () => { + scopedContainer.resolve(Scoped); +}); diff --git a/packages/@wroud/di-tools-benchmark/src/tsyringe/get_singleton.ts b/packages/@wroud/di-tools-benchmark/src/tsyringe/get_singleton.ts new file mode 100644 index 0000000..fdbd64b --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/tsyringe/get_singleton.ts @@ -0,0 +1,12 @@ +import { bench } from "vitest"; +import "reflect-metadata"; +import { container, singleton, injectable } from "tsyringe"; + +@singleton() +@injectable() +class Singleton {} + +container.resolve(Singleton); +bench("[tsyringe]", () => { + container.resolve(Singleton); +}); diff --git a/packages/@wroud/di-tools-benchmark/src/tsyringe/get_transient.ts b/packages/@wroud/di-tools-benchmark/src/tsyringe/get_transient.ts new file mode 100644 index 0000000..7150876 --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/tsyringe/get_transient.ts @@ -0,0 +1,11 @@ +import { bench } from "vitest"; +import "reflect-metadata"; +import { container, scoped, Lifecycle, injectable } from "tsyringe"; + +@scoped(Lifecycle.ResolutionScoped) +@injectable() +class Transient {} + +bench("[tsyringe]", () => { + container.resolve(Transient); +}); diff --git a/packages/@wroud/di-tools-benchmark/src/tsyringe/register.ts b/packages/@wroud/di-tools-benchmark/src/tsyringe/register.ts new file mode 100644 index 0000000..ba27849 --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/tsyringe/register.ts @@ -0,0 +1,34 @@ +import { bench } from "vitest"; +import "reflect-metadata"; +import { container, injectable, Lifecycle } from "tsyringe"; + +@injectable() +class Service {} +const service = Symbol("singleton"); + +bench("[tsyringe] singleton", () => { + const cont = container.createChildContainer(); + cont.register( + service, + { useClass: Service }, + { lifecycle: Lifecycle.Singleton }, + ); +}); + +bench("[tsyringe] transient", () => { + const cont = container.createChildContainer(); + cont.register( + service, + { useClass: Service }, + { lifecycle: Lifecycle.Transient }, + ); +}); + +bench("[tsyringe] scoped", () => { + const cont = container.createChildContainer(); + cont.register( + service, + { useClass: Service }, + { lifecycle: Lifecycle.ContainerScoped }, + ); +}); diff --git a/packages/@wroud/di-tools-benchmark/src/tsyringe/register_1000.ts b/packages/@wroud/di-tools-benchmark/src/tsyringe/register_1000.ts new file mode 100644 index 0000000..b9e9f32 --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/tsyringe/register_1000.ts @@ -0,0 +1,46 @@ +import { bench } from "vitest"; +import "reflect-metadata"; +import { container, injectable, Lifecycle } from "tsyringe"; + +const services: { service: any; impl: any }[] = []; + +for (let i = 0; i < 1000; i++) { + @injectable() + class impl {} + const service = Symbol("singleton"); + + services.push({ service, impl }); +} + +bench("[tsyringe] singleton", () => { + const cont = container.createChildContainer(); + for (const { service, impl } of services) { + cont.register( + service, + { useClass: impl }, + { lifecycle: Lifecycle.Singleton }, + ); + } +}); + +bench("[tsyringe] transient", () => { + const cont = container.createChildContainer(); + for (const { service, impl } of services) { + cont.register( + service, + { useClass: impl }, + { lifecycle: Lifecycle.Transient }, + ); + } +}); + +bench("[tsyringe] scoped", () => { + const cont = container.createChildContainer(); + for (const { service, impl } of services) { + cont.register( + service, + { useClass: impl }, + { lifecycle: Lifecycle.ContainerScoped }, + ); + } +}); diff --git a/packages/@wroud/di-tools-benchmark/src/tsyringe/tests/createDeepServices.ts b/packages/@wroud/di-tools-benchmark/src/tsyringe/tests/createDeepServices.ts new file mode 100644 index 0000000..1409b76 --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/tsyringe/tests/createDeepServices.ts @@ -0,0 +1,38 @@ +//@ts-ignore +import { inject, injectable } from "tsyringe"; + +interface IServicePair { + service: symbol; + impl: new (...args: any[]) => any; +} + +export function createDeepServices(deep: number): { + lastService: symbol; + services: readonly IServicePair[]; +} { + const services: IServicePair[] = []; + + let lastService: symbol | null = null; + + for (let i = 0; i < deep; i++) { + const service = Symbol(`service${i}`); + let impl; + if (!lastService) { + @injectable() + class implementation {} + impl = implementation; + } else { + @injectable() + class implementation { + // @ts-ignore + constructor(@inject(lastService) public service: any) {} + } + impl = implementation; + } + + lastService = service; + services.push({ service, impl }); + } + + return { lastService: lastService!, services }; +} diff --git a/packages/@wroud/di-tools-benchmark/src/tsyringe/tests/createServicesTreeTsyringe.ts b/packages/@wroud/di-tools-benchmark/src/tsyringe/tests/createServicesTreeTsyringe.ts new file mode 100644 index 0000000..c4b509b --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/src/tsyringe/tests/createServicesTreeTsyringe.ts @@ -0,0 +1,30 @@ +import { + createServicesTree, + type IServicePair, +} from "@wroud/di-tools-benchmark/tools/createServicesTree"; +import { inject, injectable } from "tsyringe"; + +export type ServicePair = IServicePair any>; +export function createServicesTreeTsyringe( + deep: number, + level: number, + services: ServicePair[], +): ServicePair | undefined { + return createServicesTree( + deep, + level, + services, + (i) => Symbol(`service${i}`), + (i, deps) => { + class Impl { + constructor(...service: any[]) {} + } + + for (let i = 0; i < deps.length; i++) { + inject(deps[i]!)(Impl, undefined, i); + } + injectable()(Impl); + return Impl; + }, + ); +} diff --git a/packages/@wroud/di-tools-benchmark/tsconfig.json b/packages/@wroud/di-tools-benchmark/tsconfig.json new file mode 100644 index 0000000..ecc27f4 --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "@wroud/tsconfig/tsconfig.json", + "compilerOptions": { + "tsBuildInfoFile": "./lib/.tsbuildinfo", + "rootDir": "src", + "rootDirs": [ + "src" + ], + "outDir": "lib", + "incremental": true, + "composite": true + }, + "include": [ + "src/benchmark" + ], + "references": [ + { + "path": "../di" + } + ] +} diff --git a/packages/@wroud/di-tools-benchmark/tsconfig.legacy-decorators.json b/packages/@wroud/di-tools-benchmark/tsconfig.legacy-decorators.json new file mode 100644 index 0000000..6d43232 --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/tsconfig.legacy-decorators.json @@ -0,0 +1,22 @@ +{ + "extends": "@wroud/tsconfig/tsconfig.json", + "compilerOptions": { + "tsBuildInfoFile": "./lib/legacy-decorators.tsbuildinfo", + "rootDir": "src", + "rootDirs": ["src"], + "outDir": "lib", + "incremental": true, + "composite": true, + "lib": ["ES2023"], + "types": ["reflect-metadata"], + "target": "ES2023", + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/inversify", "src/tsyringe"], + "references": [ + { + "path": "../di" + } + ] +} diff --git a/packages/@wroud/di-tools-benchmark/tsconfig.modern.json b/packages/@wroud/di-tools-benchmark/tsconfig.modern.json new file mode 100644 index 0000000..545dff2 --- /dev/null +++ b/packages/@wroud/di-tools-benchmark/tsconfig.modern.json @@ -0,0 +1,18 @@ +{ + "extends": "@wroud/tsconfig/tsconfig.json", + "compilerOptions": { + "tsBuildInfoFile": "./lib/modern.tsbuildinfo", + "rootDir": "src", + "rootDirs": ["src"], + "outDir": "lib", + "target": "ES2023", + "incremental": true, + "composite": true + }, + "include": ["src/tools", "src/@wroud", "src/brandi"], + "references": [ + { + "path": "../di" + } + ] +} diff --git a/packages/@wroud/di/src/di/ServiceCollection.ts b/packages/@wroud/di/src/di/ServiceCollection.ts index 3965880..bc38aef 100644 --- a/packages/@wroud/di/src/di/ServiceCollection.ts +++ b/packages/@wroud/di/src/di/ServiceCollection.ts @@ -8,6 +8,7 @@ import type { IServiceImplementationResolver, SingleServiceImplementation, SingleServiceType, + IServiceCollectionElement, } from "../types/index.js"; import { IServiceProvider } from "./IServiceProvider.js"; import { ServiceLifetime } from "./ServiceLifetime.js"; @@ -17,7 +18,7 @@ import { RegistryServiceImplementationResolver } from "../implementation-resolve import { ValueServiceImplementationResolver } from "../implementation-resolvers/ValueServiceImplementationResolver.js"; export class ServiceCollection implements IServiceCollection { - protected readonly collection: Map[]>; + protected readonly collection: Map>; constructor(collection?: ServiceCollection) { this.collection = new Map(collection?.copy() || []); if (!this.collection.has(IServiceProvider)) { @@ -27,20 +28,22 @@ export class ServiceCollection implements IServiceCollection { *[Symbol.iterator](): Iterator, any, undefined> { for (const descriptors of this.collection.values()) { - for (const descriptor of descriptors) { + for (const descriptor of descriptors.all) { yield descriptor; } } } - getDescriptors(service: SingleServiceType): IServiceDescriptor[] { - return (this.collection.get(service) ?? []) as IServiceDescriptor[]; + getDescriptors( + service: SingleServiceType, + ): readonly IServiceDescriptor[] { + return (this.collection.get(service)?.all ?? []) as IServiceDescriptor[]; } getDescriptor(service: SingleServiceType): IServiceDescriptor { - const descriptors = this.getDescriptors(service); + const descriptor = this.collection.get(service); - if (descriptors.length === 0) { + if (!descriptor) { let name = getNameOfServiceType(service); const metadata = ServiceRegistry.get(service); @@ -52,7 +55,7 @@ export class ServiceCollection implements IServiceCollection { throw new Error(`No service of type "${name}" is registered`); } - return descriptors[descriptors.length - 1]!; + return descriptor.single as IServiceDescriptor; } addScoped(service: SingleServiceImplementation): this; @@ -125,9 +128,16 @@ export class ServiceCollection implements IServiceCollection { return this; } - protected *copy(): Iterable<[any, IServiceDescriptor[]]> { + protected *copy(): Iterable<[any, IServiceCollectionElement]> { for (const [key, descriptors] of this.collection) { - yield [key, [...descriptors.map((d) => ({ ...d }))]]; + const all = [...descriptors.all.map((d) => ({ ...d }))]; + yield [ + key, + { + single: all[all.length - 1]!, + all, + }, + ]; } } @@ -147,15 +157,22 @@ export class ServiceCollection implements IServiceCollection { resolver = new ValueServiceImplementationResolver(implementation); } - const descriptors = this.collection.get(service) ?? []; - this.collection.set(service, [ - ...descriptors, - { - service, - lifetime, - resolver, - }, - ]); + const descriptor: IServiceDescriptor = { + lifetime, + service, + resolver, + }; + + let descriptors = this.collection.get(service); + if (!descriptors) { + this.collection.set(service, { + single: descriptor, + all: [descriptor], + }); + } else { + descriptors.all.push(descriptor); + descriptors.single = descriptor; + } return this; } diff --git a/packages/@wroud/di/src/di/ServiceContainerBuilder.development.test.ts b/packages/@wroud/di/src/di/ServiceContainerBuilder.development.test.ts index d66be5f..0ee5cb1 100644 --- a/packages/@wroud/di/src/di/ServiceContainerBuilder.development.test.ts +++ b/packages/@wroud/di/src/di/ServiceContainerBuilder.development.test.ts @@ -4,6 +4,8 @@ import "../debugDevelopment.js"; import { single } from "../service-type-resolvers/single.js"; import { ServiceContainerBuilder } from "./ServiceContainerBuilder.js"; import { lazy } from "../implementation-resolvers/lazy.js"; +import { createService } from "./createService.js"; +import { createAsyncMockedService } from "../tests/createAsyncMockedService.js"; describe("ServiceContainerBuilder development", () => { it("should warn async services not validated", () => { @@ -71,9 +73,47 @@ describe("ServiceContainerBuilder development", () => { }); const builder = new ServiceContainerBuilder(); - builder.addScoped(Test1, Test2); + builder.addScoped(Test1, Test2).addScoped( + Test2, + lazy(() => Promise.resolve(Test2)), + ); expect(() => builder.build()).toThrowError( "Cyclic dependency detected: Test2 (Test1) -> Test2 (Test1)", ); }); + it("should detect async cyclic dependencies", async () => { + const Test1Service = createService("Test1"); + const Test2Service = createService("Test2"); + + const Test1 = createAsyncMockedService("Test1", () => [ + single(Test2Service), + ]); + const Test2 = createAsyncMockedService("Test2", () => [ + single(Test1Service), + ]); + const Test3 = createAsyncMockedService("Test3", () => []); + const Test4 = createAsyncMockedService("Test4", () => []); + + const builder = new ServiceContainerBuilder(); + builder + .addSingleton(Test3) + .addTransient(Test4) + .addScoped( + Test1Service, + lazy(() => Promise.resolve(Test1)), + ) + .addScoped( + Test2Service, + lazy(() => Promise.resolve(Test2)), + ); + + await expect(() => builder.validate()).rejects.toThrowError( + "Cyclic dependency detected: Test1 -> Test2 -> Test1", + ); + + expect(Test1.constructorMock).not.toHaveBeenCalled(); + expect(Test2.constructorMock).not.toHaveBeenCalled(); + expect(Test3.constructorMock).not.toHaveBeenCalled(); + expect(Test4.constructorMock).not.toHaveBeenCalled(); + }); }); diff --git a/packages/@wroud/di/src/di/ServiceContainerBuilder.test.ts b/packages/@wroud/di/src/di/ServiceContainerBuilder.test.ts index 7a2fe58..403975f 100644 --- a/packages/@wroud/di/src/di/ServiceContainerBuilder.test.ts +++ b/packages/@wroud/di/src/di/ServiceContainerBuilder.test.ts @@ -1,11 +1,7 @@ import { expect, it, describe } from "vitest"; import { ServiceContainerBuilder } from "./ServiceContainerBuilder.js"; import { ServiceCollection } from "./ServiceCollection.js"; -import { lazy } from "../implementation-resolvers/lazy.js"; -import { single } from "../service-type-resolvers/single.js"; import { createSyncMockedService } from "../tests/createSyncMockedService.js"; -import { createAsyncMockedService } from "../tests/createAsyncMockedService.js"; -import { createService } from "./createService.js"; describe("ServiceContainerBuilder", () => { it("should be defined", () => { @@ -25,41 +21,6 @@ describe("ServiceContainerBuilder", () => { 'No service of type "String" is registered', ); }); - it("should detect async cyclic dependencies", async () => { - const Test1Service = createService("Test1"); - const Test2Service = createService("Test2"); - - const Test1 = createAsyncMockedService("Test1", () => [ - single(Test2Service), - ]); - const Test2 = createAsyncMockedService("Test2", () => [ - single(Test1Service), - ]); - const Test3 = createAsyncMockedService("Test3", () => []); - const Test4 = createAsyncMockedService("Test4", () => []); - - const builder = new ServiceContainerBuilder(); - builder - .addSingleton(Test3) - .addTransient(Test4) - .addScoped( - Test1Service, - lazy(() => Promise.resolve(Test1)), - ) - .addScoped( - Test2Service, - lazy(() => Promise.resolve(Test2)), - ); - - await expect(() => builder.validate()).rejects.toThrowError( - "Cyclic dependency detected: Test1 -> Test2 -> Test1", - ); - - expect(Test1.constructorMock).not.toHaveBeenCalled(); - expect(Test2.constructorMock).not.toHaveBeenCalled(); - expect(Test3.constructorMock).not.toHaveBeenCalled(); - expect(Test4.constructorMock).not.toHaveBeenCalled(); - }); it("validate should have no side effects", async () => { const Test1 = createSyncMockedService("Test1", () => []); const Test2 = createSyncMockedService("Test2", () => []); @@ -70,9 +31,6 @@ describe("ServiceContainerBuilder", () => { await builder.validate(); - for (const descriptor of builder) { - expect(descriptor.dry).not.toBeDefined(); - } expect(Test1.constructorMock).not.toHaveBeenCalled(); expect(Test2.constructorMock).not.toHaveBeenCalled(); expect(Test3.constructorMock).not.toHaveBeenCalled(); diff --git a/packages/@wroud/di/src/di/ServiceContainerBuilder.ts b/packages/@wroud/di/src/di/ServiceContainerBuilder.ts index c93984a..e911291 100644 --- a/packages/@wroud/di/src/di/ServiceContainerBuilder.ts +++ b/packages/@wroud/di/src/di/ServiceContainerBuilder.ts @@ -1,4 +1,7 @@ import { Debug } from "../debug.js"; +import { DryImplementationResolver } from "../implementation-resolvers/DryImplementationResolver.js"; +import { isAsyncServiceImplementationResolver } from "../implementation-resolvers/isAsyncServiceImplementation.js"; +import { ValueServiceImplementationResolver } from "../implementation-resolvers/ValueServiceImplementationResolver.js"; import { IServiceProvider } from "./IServiceProvider.js"; import { ServiceCollection } from "./ServiceCollection.js"; import { ServiceProvider } from "./ServiceProvider.js"; @@ -17,7 +20,8 @@ export class ServiceContainerBuilder extends ServiceCollection { const provider = new ServiceProvider(collectionCopy).createAsyncScope(); for (const descriptor of collectionCopy) { - descriptor.dry = true; + // @ts-expect-error + descriptor.resolver = new DryImplementationResolver(descriptor.resolver); } for (const descriptor of collectionCopy) { @@ -33,7 +37,14 @@ export class ServiceContainerBuilder extends ServiceCollection { const provider = new ServiceProvider(collectionCopy).createScope(); for (const descriptor of collectionCopy) { - descriptor.dry = true; + if (isAsyncServiceImplementationResolver(descriptor.resolver)) { + // @ts-expect-error + descriptor.resolver = new ValueServiceImplementationResolver(null); + } + // @ts-expect-error + descriptor.resolver = new DryImplementationResolver( + descriptor.resolver, + ); } for (const descriptor of collectionCopy) { diff --git a/packages/@wroud/di/src/di/ServiceInstanceInfo.ts b/packages/@wroud/di/src/di/ServiceInstanceInfo.ts index c04a6bd..d505a91 100644 --- a/packages/@wroud/di/src/di/ServiceInstanceInfo.ts +++ b/packages/@wroud/di/src/di/ServiceInstanceInfo.ts @@ -4,6 +4,7 @@ import type { } from "../types/index.js"; import { getNameOfDescriptor } from "../helpers/getNameOfDescriptor.js"; +const NOT_INITIALIZED = Symbol("NOT_INITIALIZED"); export class ServiceInstanceInfo implements IServiceInstanceInfo { get instance(): T { if (!this.initialized) { @@ -12,39 +13,46 @@ export class ServiceInstanceInfo implements IServiceInstanceInfo { ); } - return this._instance!; + return this._instance as T; + } + + get initialized(): boolean { + return this._instance !== NOT_INITIALIZED; } - initialized: boolean; disposed: boolean; - dependents: Set>; - private _instance: T | undefined; + dependents: IServiceInstanceInfo[]; + private _instance: T | typeof NOT_INITIALIZED; constructor(public descriptor: IServiceDescriptor) { - this.initialized = false; this.disposed = false; - this.dependents = new Set(); + this.dependents = []; + this._instance = NOT_INITIALIZED; } - initialize(creator: () => T): void { - if (this.initialized) { - return; + *getInstance(): Generator { + return this._instance; + } + + initialize(creator: () => T): T { + if (this._instance === NOT_INITIALIZED) { + this._instance = creator(); } - this._instance = this.descriptor.dry ? (null as T) : creator(); - this.initialized = true; + + return this._instance; } addDependent(dependent: IServiceInstanceInfo): void { - this.dependents.add(dependent); + this.dependents.push(dependent); } disposeSync(): void { - const instance = this.instance as any; + if (this.disposed || !this.initialized) { + return; + } + + const instance = this._instance as any; const disposeMethod = instance?.[Symbol.dispose] ?? instance?.dispose; - if ( - typeof disposeMethod === "function" && - !this.disposed && - this.initialized - ) { + if (typeof disposeMethod === "function") { for (const dependent of this.dependents) { dependent.disposeSync(); } @@ -55,25 +63,25 @@ export class ServiceInstanceInfo implements IServiceInstanceInfo { } async disposeAsync(): Promise { - const instance = this.instance as any; + if (this.disposed || !this.initialized) { + return; + } + const instance = this._instance as any; - if (!this.disposed && this.initialized) { - const disposeMethod = - instance?.[Symbol.asyncDispose] ?? instance?.dispose; - if (typeof disposeMethod === "function") { - this.disposed = true; - try { - await Promise.all( - [...this.dependents].map((dependent) => dependent.disposeAsync()), - ); - await Reflect.apply(disposeMethod, instance, []); - } catch (e) { - this.disposed = false; - throw e; - } - } else { - this.disposeSync(); + const disposeMethod = instance?.[Symbol.asyncDispose] ?? instance?.dispose; + if (typeof disposeMethod === "function") { + this.disposed = true; + try { + await Promise.all( + [...this.dependents].map((dependent) => dependent.disposeAsync()), + ); + await Reflect.apply(disposeMethod, instance, []); + } catch (e) { + this.disposed = false; + throw e; } + } else { + this.disposeSync(); } } } diff --git a/packages/@wroud/di/src/di/ServiceInstancesStore.test.ts b/packages/@wroud/di/src/di/ServiceInstancesStore.test.ts index f6f74d2..b0525cf 100644 --- a/packages/@wroud/di/src/di/ServiceInstancesStore.test.ts +++ b/packages/@wroud/di/src/di/ServiceInstancesStore.test.ts @@ -15,9 +15,9 @@ describe("ServiceInstancesStore", () => { }; const instance1 = {}; - const instanceInfo = store.addInstance(descriptor); + const instanceInfo = store.addInstance(descriptor, null); instanceInfo.initialize(() => instance1); - expect(store.addInstance(descriptor)).toBe(instanceInfo); + expect(store.addInstance(descriptor, null)).toBe(instanceInfo); }); it("should has", async () => { const store = new ServiceInstancesStore(); @@ -27,7 +27,7 @@ describe("ServiceInstancesStore", () => { lifetime: ServiceLifetime.Singleton, }; - store.addInstance(descriptor); + store.addInstance(descriptor, null); expect(store.hasInstanceOf(descriptor)).toBe(true); expect( store.hasInstanceOf({ diff --git a/packages/@wroud/di/src/di/ServiceInstancesStore.ts b/packages/@wroud/di/src/di/ServiceInstancesStore.ts index fcead82..e8828b1 100644 --- a/packages/@wroud/di/src/di/ServiceInstancesStore.ts +++ b/packages/@wroud/di/src/di/ServiceInstancesStore.ts @@ -31,7 +31,7 @@ export class ServiceInstancesStore implements IServiceInstancesStore { addInstance( descriptor: IServiceDescriptor, - requestedBy?: IServiceDescriptor, + requestedBy: IServiceDescriptor | null, ): IServiceInstanceInfo { let instanceInfo = this.getInstanceInfo(descriptor); diff --git a/packages/@wroud/di/src/di/ServiceProvider.async.development.test.ts b/packages/@wroud/di/src/di/ServiceProvider.async.development.test.ts new file mode 100644 index 0000000..af2940b --- /dev/null +++ b/packages/@wroud/di/src/di/ServiceProvider.async.development.test.ts @@ -0,0 +1,39 @@ +/// +import { describe, expect, it } from "vitest"; +import { ServiceContainerBuilder } from "./ServiceContainerBuilder.js"; +import { lazy } from "../implementation-resolvers/lazy.js"; +import { createService } from "./createService.js"; +import { createSyncMockedService } from "../tests/createSyncMockedService.js"; +import "../debugDevelopment.js"; + +describe("ServiceProvider", () => { + it("should throw on attempt to resolve async service implementation with circular dependency", async () => { + const serviceB = createService("B"); + const A = createSyncMockedService("A", () => [serviceB]); + const loaderA = lazy(() => Promise.resolve(A)); + const B = createSyncMockedService("B", () => [A]); + const loaderB = lazy(() => Promise.resolve(B)); + + const serviceProvider = new ServiceContainerBuilder() + .addSingleton(A, loaderA) + .addSingleton(serviceB, loaderB) + .build(); + + await expect(() => serviceProvider.getServiceAsync(A)).rejects.toThrowError( + "Cyclic dependency detected: A -> B -> A", + ); + }); + it("should throw on attempt to resolve async service implementation with circular dependency with multiple services resolve", async () => { + const serviceB = createService("B"); + const A = createSyncMockedService("A", () => [[serviceB]]); + const loaderA = lazy(() => Promise.resolve(A)); + + const serviceProvider = new ServiceContainerBuilder() + .addSingleton(serviceB, loaderA) + .build(); + + await expect(() => + serviceProvider.getServicesAsync(serviceB), + ).rejects.toThrowError("Cyclic dependency detected: A (B) -> A (B)"); + }); +}); diff --git a/packages/@wroud/di/src/di/ServiceProvider.async.test.ts b/packages/@wroud/di/src/di/ServiceProvider.async.test.ts index c735b03..bcacba5 100644 --- a/packages/@wroud/di/src/di/ServiceProvider.async.test.ts +++ b/packages/@wroud/di/src/di/ServiceProvider.async.test.ts @@ -286,35 +286,6 @@ describe("ServiceProvider", () => { expect(a2).toBeInstanceOf(A); expect(a1).toBe(a2); }); - it("should throw on attempt to resolve async service implementation with circular dependency", async () => { - const serviceB = createService("B"); - const A = createSyncMockedService("A", () => [serviceB]); - const loaderA = lazy(() => Promise.resolve(A)); - const B = createSyncMockedService("B", () => [A]); - const loaderB = lazy(() => Promise.resolve(B)); - - const serviceProvider = new ServiceContainerBuilder() - .addSingleton(A, loaderA) - .addSingleton(serviceB, loaderB) - .build(); - - await expect(() => serviceProvider.getServiceAsync(A)).rejects.toThrowError( - "Cyclic dependency detected: A -> B -> A", - ); - }); - it("should throw on attempt to resolve async service implementation with circular dependency with multiple services resolve", async () => { - const serviceB = createService("B"); - const A = createSyncMockedService("A", () => [[serviceB]]); - const loaderA = lazy(() => Promise.resolve(A)); - - const serviceProvider = new ServiceContainerBuilder() - .addSingleton(serviceB, loaderA) - .build(); - - await expect(() => - serviceProvider.getServicesAsync(serviceB), - ).rejects.toThrowError("Cyclic dependency detected: A (B) -> A (B)"); - }); it("should not initialize copy of previously resolved service when resolving multiple services async", async () => { const A = createSyncMockedService("A"); const B = createSyncMockedService("B"); diff --git a/packages/@wroud/di/src/di/ServiceProvider.static.test.ts b/packages/@wroud/di/src/di/ServiceProvider.static.test.ts index 448eb42..7d33799 100644 --- a/packages/@wroud/di/src/di/ServiceProvider.static.test.ts +++ b/packages/@wroud/di/src/di/ServiceProvider.static.test.ts @@ -12,7 +12,8 @@ describe("ServiceProvider", () => { ServiceProvider.internalGetService( {} as any, single(createService("test")), - new Set(), + null, + { value: null, next: null }, "sync", ), ).toThrowError("provider must be an instance of ServiceProvider"); diff --git a/packages/@wroud/di/src/di/ServiceProvider.ts b/packages/@wroud/di/src/di/ServiceProvider.ts index 0040b44..658f226 100644 --- a/packages/@wroud/di/src/di/ServiceProvider.ts +++ b/packages/@wroud/di/src/di/ServiceProvider.ts @@ -12,6 +12,8 @@ import type { IResolverServiceType, SingleServiceType, ServiceType, + RequestPath, + IRequestPathNode, } from "../types/index.js"; import { resolveGeneratorAsync } from "../helpers/resolveGeneratorAsync.js"; import { resolveGeneratorSync } from "../helpers/resolveGeneratorSync.js"; @@ -19,18 +21,30 @@ import { isServiceProvider } from "../helpers/isServiceProvider.js"; import { all } from "../service-type-resolvers/all.js"; import { single } from "../service-type-resolvers/single.js"; import { isServiceTypeResolver } from "../service-type-resolvers/BaseServiceTypeResolver.js"; +import { Debug } from "../debug.js"; + +const EMPTY_PATH: IRequestPathNode | null> = { + value: null, + next: null, +}; export class ServiceProvider implements IServiceProvider { static internalGetService( provider: IServiceProvider, service: IResolverServiceType, - requestedBy: Set>, + requestedBy: IServiceDescriptor | null, + requestedPath: RequestPath, mode: "sync" | "async", ): Generator, T, unknown> { if (!(provider instanceof ServiceProvider)) { throw new Error("provider must be an instance of ServiceProvider"); } - return provider.internalGetService(service, requestedBy, mode); + return provider.internalGetService( + service, + requestedBy, + requestedPath, + mode, + ); } static getDescriptor( @@ -46,7 +60,7 @@ export class ServiceProvider implements IServiceProvider { static getDescriptors( provider: IServiceProvider, service: SingleServiceType, - ): IServiceDescriptor[] { + ): readonly IServiceDescriptor[] { if (!(provider instanceof ServiceProvider)) { throw new Error("provider must be an instance of ServiceProvider"); } @@ -66,7 +80,7 @@ export class ServiceProvider implements IServiceProvider { getServices(service: ServiceType): T[] { return resolveGeneratorSync( - this.internalGetService(all(service), new Set(), "sync"), + this.internalGetService(all(service), null, EMPTY_PATH, "sync"), ); } @@ -76,7 +90,7 @@ export class ServiceProvider implements IServiceProvider { } return resolveGeneratorAsync( - this.internalGetService(service, new Set(), "async"), + this.internalGetService(service, null, EMPTY_PATH, "async"), ); } @@ -86,13 +100,13 @@ export class ServiceProvider implements IServiceProvider { } return resolveGeneratorSync( - this.internalGetService(service, new Set(), "sync"), + this.internalGetService(service, null, EMPTY_PATH, "sync"), ); } getServicesAsync(service: ServiceType): Promise { return resolveGeneratorAsync( - this.internalGetService(all(service), new Set(), "async"), + this.internalGetService(all(service), null, EMPTY_PATH, "async"), ); } @@ -118,95 +132,100 @@ export class ServiceProvider implements IServiceProvider { }; } - private *internalGetService( + private internalGetService( service: IResolverServiceType, - requestedBy: Set>, + requestedBy: IServiceDescriptor | null, + requestedPath: RequestPath, mode: "sync" | "async", ): Generator, T, unknown> { - return yield* service.resolve( + return service.resolve( this.collection, this.instancesStore, this.resolveServiceImplementation, requestedBy, + requestedPath, mode, ); } - private *resolveServiceImplementation( + private resolveServiceImplementation( descriptor: IServiceDescriptor, - requestedBy: Set>, + requestedBy: IServiceDescriptor | null, + requestedPath: RequestPath, mode: "sync" | "async", ): Generator, T, unknown> { - if (!this.instancesStore.getInstanceInfo(descriptor)?.initialized) { - validateRequestPath(requestedBy, descriptor); + if (descriptor.lifetime === ServiceLifetime.Singleton && this.parent) { + return (this.parent as ServiceProvider).resolveServiceImplementation( + descriptor, + requestedBy, + requestedPath, + mode, + ); } - if (descriptor.lifetime === ServiceLifetime.Singleton && this.parent) { - return yield* ( - this.parent as ServiceProvider - ).resolveServiceImplementation(descriptor, requestedBy, mode); + if (descriptor.lifetime !== ServiceLifetime.Transient) { + const instanceInfo = this.instancesStore.getInstanceInfo(descriptor); + + if (instanceInfo?.initialized) { + return instanceInfo.getInstance() as Generator; + } + } + + if (Debug.extended) { + validateRequestPath(requestedPath, descriptor); } - return yield* this.createInstanceFromDescriptor( + return this.createInstanceFromDescriptor( descriptor, requestedBy, + requestedPath, mode, ); } private *createInstanceFromDescriptor( descriptor: IServiceDescriptor, - requestedBy: Set>, + requestedBy: IServiceDescriptor | null, + requestedPath: RequestPath, mode: "sync" | "async", ): Generator, T, unknown> { try { - const lastRequestedBy = [...requestedBy].pop(); + if (descriptor.lifetime === ServiceLifetime.Transient) { + if (isServiceProvider(descriptor.service)) { + return this as unknown as T; + } + + return (yield* descriptor.resolver.resolve( + this.internalGetService, + descriptor, + requestedBy, + requestedPath, + mode, + ))(); + } if (descriptor.lifetime === ServiceLifetime.Scoped) { - if (lastRequestedBy?.lifetime === ServiceLifetime.Singleton) { + if (requestedBy?.lifetime === ServiceLifetime.Singleton) { throw new Error( `Scoped service cannot be resolved from singleton service.`, ); } - if (!this.parent) { throw new Error("Scoped services require a service scope."); } } - switch (descriptor.lifetime) { - case ServiceLifetime.Scoped: - case ServiceLifetime.Singleton: { - const instanceInfo = this.instancesStore.addInstance( - descriptor, - lastRequestedBy, - ); - - instanceInfo.initialize( - yield* descriptor.resolver.resolve( - this.internalGetService, - descriptor, - requestedBy, - mode, - ), - ); - - return instanceInfo.instance; - } - case ServiceLifetime.Transient: { - if (isServiceProvider(descriptor.service)) { - return this as unknown as T; - } - - const implementationGetter = yield* descriptor.resolver.resolve( + return this.instancesStore + .addInstance(descriptor, requestedBy) + .initialize( + yield* descriptor.resolver.resolve( this.internalGetService, descriptor, requestedBy, + requestedPath, mode, - ); - return descriptor.dry ? (null as T) : implementationGetter(); - } - } + ), + ); } catch (exception: any) { throw new Error( `Failed to initiate service ${getNameOfDescriptor(descriptor)}:\n\r${exception.message}`, diff --git a/packages/@wroud/di/src/di/ServiceRegistry.ts b/packages/@wroud/di/src/di/ServiceRegistry.ts index bd6df56..92c8a63 100644 --- a/packages/@wroud/di/src/di/ServiceRegistry.ts +++ b/packages/@wroud/di/src/di/ServiceRegistry.ts @@ -5,27 +5,27 @@ import type { } from "../types/index.js"; export class ServiceRegistry { - private static readonly services: WeakMap = - new WeakMap(); + private static metadataKey = Symbol("service-metadata"); static register< TClass extends abstract new (...args: MapToServicesType) => any, TServices extends IResolverServiceType[] = [], >(service: TClass, metadata: IServiceMetadata) { - const existing = this.services.get(service); - - if (existing) { - throw new Error(`Service ${existing.name} is already registered`); + if (ServiceRegistry.has(service)) { + throw new Error(`Service ${service.name} is already registered`); } - this.services.set(service, metadata); + Object.defineProperty(service, this.metadataKey, { + value: metadata, + writable: false, + }); } static has(service: any): boolean { - return this.services.has(service); + return this.metadataKey in service; } static get(service: any): IServiceMetadata | undefined { - return this.services.get(service); + return service[this.metadataKey]; } } diff --git a/packages/@wroud/di/src/di/validation/validateRequestPath.ts b/packages/@wroud/di/src/di/validation/validateRequestPath.ts index a38b446..a377f85 100644 --- a/packages/@wroud/di/src/di/validation/validateRequestPath.ts +++ b/packages/@wroud/di/src/di/validation/validateRequestPath.ts @@ -1,15 +1,20 @@ -import type { IServiceDescriptor } from "../../types/IServiceDescriptor.js"; +import type { IServiceDescriptor, RequestPath } from "../../types/index.js"; import { getNameOfDescriptor } from "../../helpers/getNameOfDescriptor.js"; +import { requestPathToArray } from "../../helpers/requestPathToArray.js"; export function validateRequestPath( - path: Set>, + path: RequestPath, descriptor: IServiceDescriptor, ) { - if (path.has(descriptor)) { - throw new Error( - `Cyclic dependency detected: ${[...path, descriptor] - .map(getNameOfDescriptor) - .join(" -> ")}`, - ); + for (let node: RequestPath | null = path; node; node = node.next) { + if (node.value === descriptor) { + throw new Error( + `Cyclic dependency detected: ${[descriptor, ...requestPathToArray(path)] + .reverse() + .filter((v) => v !== null) + .map(getNameOfDescriptor) + .join(" -> ")}`, + ); + } } } diff --git a/packages/@wroud/di/src/helpers/requestPathToArray.ts b/packages/@wroud/di/src/helpers/requestPathToArray.ts new file mode 100644 index 0000000..b1c9567 --- /dev/null +++ b/packages/@wroud/di/src/helpers/requestPathToArray.ts @@ -0,0 +1,11 @@ +import type { IRequestPathNode } from "../types/IRequestPathNode.js"; + +export function* requestPathToArray( + node: IRequestPathNode | null, +): Generator { + let current: IRequestPathNode | null = node; + while (current !== null) { + yield current.value; + current = current.next; + } +} diff --git a/packages/@wroud/di/src/helpers/resolveGeneratorAsync.ts b/packages/@wroud/di/src/helpers/resolveGeneratorAsync.ts index 96d1c3f..3171672 100644 --- a/packages/@wroud/di/src/helpers/resolveGeneratorAsync.ts +++ b/packages/@wroud/di/src/helpers/resolveGeneratorAsync.ts @@ -1,17 +1,13 @@ export async function resolveGeneratorAsync( iterator: Generator, TResult, unknown>, ) { - let result: IteratorResult, TResult>; + let result = iterator.next(); - while (!(result = iterator.next()).done) { + for (; !result.done; result = iterator.next()) { try { await result.value; } catch (err) { result = iterator.throw(err); - - if (result.done) { - break; - } } } diff --git a/packages/@wroud/di/src/helpers/resolveGeneratorSync.ts b/packages/@wroud/di/src/helpers/resolveGeneratorSync.ts index 7781a0a..04e2006 100644 --- a/packages/@wroud/di/src/helpers/resolveGeneratorSync.ts +++ b/packages/@wroud/di/src/helpers/resolveGeneratorSync.ts @@ -3,14 +3,10 @@ import { Debug } from "../debug.js"; export function resolveGeneratorSync( iterator: Generator, TResult, unknown>, ) { - let result: IteratorResult, TResult>; + let result = iterator.next(); - while (!(result = iterator.next()).done) { + for (; !result.done; result = iterator.next()) { result = iterator.throw(new Error(Debug.errors.lazyServiceCantResolveSync)); - - if (result.done) { - break; - } } return result.value; diff --git a/packages/@wroud/di/src/helpers/tryResolveService.ts b/packages/@wroud/di/src/helpers/tryResolveService.ts deleted file mode 100644 index 714e346..0000000 --- a/packages/@wroud/di/src/helpers/tryResolveService.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ServiceCollection } from "../di/ServiceCollection.js"; -import { ServiceProvider } from "../di/ServiceProvider.js"; -import type { SingleServiceType } from "../types/SingleServiceType.js"; - -export function tryResolveService( - collection: ServiceCollection, - service: SingleServiceType, -) { - const collectionCopy = new ServiceCollection(collection); - const provider = new ServiceProvider(collectionCopy).createScope(); - - for (const descriptor of collectionCopy) { - descriptor.dry = true; - } - - provider.serviceProvider.getService(service); - provider[Symbol.dispose](); -} diff --git a/packages/@wroud/di/src/implementation-resolvers/AsyncServiceImplementationResolver.ts b/packages/@wroud/di/src/implementation-resolvers/AsyncServiceImplementationResolver.ts index 786f6eb..2e08983 100644 --- a/packages/@wroud/di/src/implementation-resolvers/AsyncServiceImplementationResolver.ts +++ b/packages/@wroud/di/src/implementation-resolvers/AsyncServiceImplementationResolver.ts @@ -4,11 +4,11 @@ import type { IServiceDescriptor, IServiceImplementationResolver, IServiceTypeResolver, + RequestPath, } from "../types/index.js"; import { Debug } from "../debug.js"; import { BaseServiceImplementationResolver } from "./BaseServiceImplementationResolver.js"; import { RegistryServiceImplementationResolver } from "./RegistryServiceImplementationResolver.js"; -import { ValueServiceImplementationResolver } from "./ValueServiceImplementationResolver.js"; import { AsyncServiceImplementationError } from "../di/errors/AsyncServiceImplementationError.js"; const NOT_LOADED = Symbol("NOT_LOADED"); @@ -38,18 +38,10 @@ export class AsyncServiceImplementationResolver< *resolve( internalGetService: IServiceTypeResolver, descriptor: IServiceDescriptor, - requestedBy: Set>, + requestedBy: IServiceDescriptor | null, + requestedPath: RequestPath, mode: "sync" | "async", ): Generator, IResolvedServiceImplementation, unknown> { - if (mode === "sync" && descriptor.dry) { - return yield* new ValueServiceImplementationResolver(null as T).resolve( - internalGetService, - descriptor, - requestedBy, - mode, - ); - } - if (this.implementation === NOT_LOADED || mode === "sync") { yield this.load(); @@ -62,6 +54,7 @@ export class AsyncServiceImplementationResolver< internalGetService, descriptor, requestedBy, + requestedPath, mode, ); } diff --git a/packages/@wroud/di/src/implementation-resolvers/BaseServiceImplementationResolver.ts b/packages/@wroud/di/src/implementation-resolvers/BaseServiceImplementationResolver.ts index 4b2612f..050edaf 100644 --- a/packages/@wroud/di/src/implementation-resolvers/BaseServiceImplementationResolver.ts +++ b/packages/@wroud/di/src/implementation-resolvers/BaseServiceImplementationResolver.ts @@ -3,6 +3,7 @@ import type { IServiceDescriptor, IServiceImplementationResolver, IServiceTypeResolver, + RequestPath, } from "../types/index.js"; export abstract class BaseServiceImplementationResolver @@ -12,7 +13,8 @@ export abstract class BaseServiceImplementationResolver abstract resolve( internalGetService: IServiceTypeResolver, descriptor: IServiceDescriptor, - requestedBy: Set>, + requestedBy: IServiceDescriptor | null, + requestedPath: RequestPath, mode: "sync" | "async", ): Generator, IResolvedServiceImplementation, unknown>; } diff --git a/packages/@wroud/di/src/implementation-resolvers/DryImplementationResolver.ts b/packages/@wroud/di/src/implementation-resolvers/DryImplementationResolver.ts new file mode 100644 index 0000000..59ae8c9 --- /dev/null +++ b/packages/@wroud/di/src/implementation-resolvers/DryImplementationResolver.ts @@ -0,0 +1,44 @@ +import { getNameOfServiceType } from "../helpers/getNameOfServiceType.js"; +import type { + IResolvedServiceImplementation, + IServiceDescriptor, + IServiceImplementationResolver, + IServiceTypeResolver, + RequestPath, + SingleServiceImplementation, +} from "../types/index.js"; +import { BaseServiceImplementationResolver } from "./BaseServiceImplementationResolver.js"; + +export class DryImplementationResolver< + T, +> extends BaseServiceImplementationResolver { + get name(): string { + return getNameOfServiceType(this.implementation); + } + + constructor( + private readonly implementation: IServiceImplementationResolver< + T | SingleServiceImplementation + >, + ) { + super(); + } + + *resolve( + internalGetService: IServiceTypeResolver, + descriptor: IServiceDescriptor, + requestedBy: IServiceDescriptor | null, + requestedPath: RequestPath, + mode: "sync" | "async", + ): Generator, IResolvedServiceImplementation, unknown> { + yield* this.implementation.resolve( + internalGetService, + descriptor, + requestedBy, + requestedPath, + mode, + ); + + return () => null as T; + } +} diff --git a/packages/@wroud/di/src/implementation-resolvers/FactoryServiceImplementationResolver.ts b/packages/@wroud/di/src/implementation-resolvers/FactoryServiceImplementationResolver.ts index ca9a64e..709268f 100644 --- a/packages/@wroud/di/src/implementation-resolvers/FactoryServiceImplementationResolver.ts +++ b/packages/@wroud/di/src/implementation-resolvers/FactoryServiceImplementationResolver.ts @@ -6,6 +6,7 @@ import type { IServiceImplementationResolver, SingleServiceImplementation, IServiceTypeResolver, + RequestPath, } from "../types/index.js"; import { BaseServiceImplementationResolver, @@ -35,7 +36,8 @@ export class FactoryServiceImplementationResolver< *resolve( getService: IServiceTypeResolver, descriptor: IServiceDescriptor, - requestedBy: Set>, + requestedBy: IServiceDescriptor | null, + requestedPath: RequestPath, mode: "sync" | "async", ): Generator, IResolvedServiceImplementation, unknown> { let implementation = this.implementation; @@ -49,6 +51,7 @@ export class FactoryServiceImplementationResolver< getService, descriptor, requestedBy, + requestedPath, mode, )) as T | SingleServiceImplementation; } @@ -57,6 +60,7 @@ export class FactoryServiceImplementationResolver< const serviceProvider = yield* getService( single(IServiceProvider), requestedBy, + requestedPath, mode, ); return () => { @@ -82,6 +86,6 @@ export class FactoryServiceImplementationResolver< return yield* new ValueServiceImplementationResolver( implementation, - ).resolve(getService, descriptor, requestedBy, mode); + ).resolve(getService, descriptor, requestedBy, requestedPath, mode); } } diff --git a/packages/@wroud/di/src/implementation-resolvers/ProxyServiceImplementationResolver.ts b/packages/@wroud/di/src/implementation-resolvers/ProxyServiceImplementationResolver.ts index 45b1ea5..b89ee14 100644 --- a/packages/@wroud/di/src/implementation-resolvers/ProxyServiceImplementationResolver.ts +++ b/packages/@wroud/di/src/implementation-resolvers/ProxyServiceImplementationResolver.ts @@ -4,6 +4,7 @@ import type { IServiceDescriptor, IResolverServiceType, IServiceTypeResolver, + RequestPath, } from "../types/index.js"; import { BaseServiceImplementationResolver } from "./BaseServiceImplementationResolver.js"; @@ -21,12 +22,14 @@ export class ProxyServiceImplementationResolver< *resolve( internalGetService: IServiceTypeResolver, descriptor: IServiceDescriptor, - requestedBy: Set>, + requestedBy: IServiceDescriptor | null, + requestedPath: RequestPath, mode: "sync" | "async", ): Generator, IResolvedServiceImplementation, unknown> { const implementation = yield* internalGetService( this.service, requestedBy, + requestedPath, mode, ); return () => implementation; diff --git a/packages/@wroud/di/src/implementation-resolvers/RegistryServiceImplementationResolver.ts b/packages/@wroud/di/src/implementation-resolvers/RegistryServiceImplementationResolver.ts index c4589dc..55fac69 100644 --- a/packages/@wroud/di/src/implementation-resolvers/RegistryServiceImplementationResolver.ts +++ b/packages/@wroud/di/src/implementation-resolvers/RegistryServiceImplementationResolver.ts @@ -6,6 +6,7 @@ import type { IServiceImplementationResolver, SingleServiceImplementation, IServiceTypeResolver, + RequestPath, } from "../types/index.js"; import { BaseServiceImplementationResolver, @@ -33,7 +34,8 @@ export class RegistryServiceImplementationResolver< *resolve( internalGetService: IServiceTypeResolver, descriptor: IServiceDescriptor, - requestedBy: Set>, + requestedBy: IServiceDescriptor | null, + requestedPath: RequestPath, mode: "sync" | "async", ): Generator, IResolvedServiceImplementation, unknown> { let implementation = this.implementation; @@ -47,6 +49,7 @@ export class RegistryServiceImplementationResolver< internalGetService, descriptor, requestedBy, + requestedPath, mode, )) as T | SingleServiceImplementation; } @@ -54,20 +57,34 @@ export class RegistryServiceImplementationResolver< const metadata = ServiceRegistry.get(implementation); if (metadata) { - const instanceDependencies: any[] = []; - requestedBy = new Set([...requestedBy, descriptor]); - for (const dependency of metadata.dependencies) { - instanceDependencies.push( - yield* internalGetService(dependency, requestedBy, mode), - ); + if (metadata.dependencies.length > 0) { + const instanceDependencies: any[] = []; + + requestedPath = { + value: descriptor, + next: requestedPath, + }; + for (const dependency of metadata.dependencies) { + instanceDependencies.push( + yield* internalGetService( + dependency, + descriptor, + requestedPath, + mode, + ), + ); + } + return () => + new (implementation as IServiceConstructor)( + ...instanceDependencies, + ); } - return () => - new (implementation as IServiceConstructor)(...instanceDependencies); + return () => new (implementation as IServiceConstructor)(); } return yield* new FactoryServiceImplementationResolver( implementation, - ).resolve(internalGetService, descriptor, requestedBy, mode); + ).resolve(internalGetService, descriptor, requestedBy, requestedPath, mode); } } diff --git a/packages/@wroud/di/src/implementation-resolvers/ValueServiceImplementationResolver.ts b/packages/@wroud/di/src/implementation-resolvers/ValueServiceImplementationResolver.ts index b221f2f..e3c8a80 100644 --- a/packages/@wroud/di/src/implementation-resolvers/ValueServiceImplementationResolver.ts +++ b/packages/@wroud/di/src/implementation-resolvers/ValueServiceImplementationResolver.ts @@ -4,6 +4,7 @@ import type { IServiceDescriptor, IServiceImplementationResolver, IServiceTypeResolver, + RequestPath, } from "../types/index.js"; import { BaseServiceImplementationResolver, @@ -26,7 +27,8 @@ export class ValueServiceImplementationResolver< *resolve( internalGetService: IServiceTypeResolver, descriptor: IServiceDescriptor, - requestedBy: Set>, + requestedBy: IServiceDescriptor | null, + requestedPath: RequestPath, mode: "sync" | "async", ): Generator, IResolvedServiceImplementation, unknown> { let implementation = this.implementation; @@ -36,6 +38,7 @@ export class ValueServiceImplementationResolver< internalGetService, descriptor, requestedBy, + requestedPath, mode, )) as T; } diff --git a/packages/@wroud/di/src/service-type-resolvers/BaseServiceTypeResolver.ts b/packages/@wroud/di/src/service-type-resolvers/BaseServiceTypeResolver.ts index 27b7592..443877e 100644 --- a/packages/@wroud/di/src/service-type-resolvers/BaseServiceTypeResolver.ts +++ b/packages/@wroud/di/src/service-type-resolvers/BaseServiceTypeResolver.ts @@ -6,6 +6,7 @@ import type { IServiceDescriptorResolver, IServiceCollection, IServiceInstancesStore, + RequestPath, } from "../types/index.js"; export abstract class BaseServiceTypeResolver @@ -27,7 +28,8 @@ export abstract class BaseServiceTypeResolver collection: IServiceCollection, instancesStore: IServiceInstancesStore, resolveServiceImplementation: IServiceDescriptorResolver, - requestedBy: Set>, + requestedBy: IServiceDescriptor | null, + requestedPath: RequestPath, mode: "sync" | "async", descriptor?: IServiceDescriptor, ): Generator, Out, unknown>; diff --git a/packages/@wroud/di/src/service-type-resolvers/ListServiceTypeResolver.ts b/packages/@wroud/di/src/service-type-resolvers/ListServiceTypeResolver.ts index d8c5b5f..c704d3d 100644 --- a/packages/@wroud/di/src/service-type-resolvers/ListServiceTypeResolver.ts +++ b/packages/@wroud/di/src/service-type-resolvers/ListServiceTypeResolver.ts @@ -4,6 +4,7 @@ import type { IServiceDescriptor, IServiceCollection, IServiceInstancesStore, + RequestPath, } from "../types/index.js"; import { BaseServiceTypeResolver, @@ -22,7 +23,8 @@ export class ListServiceTypeResolver extends BaseServiceTypeResolver< collection: IServiceCollection, instancesStore: IServiceInstancesStore, resolveServiceImplementation: IServiceDescriptorResolver, - requestedBy: Set>, + requestedBy: IServiceDescriptor | null, + requestedPath: RequestPath, mode: "sync" | "async", ): Generator, T[], unknown> { const descriptors = collection.getDescriptors(this.service); @@ -38,13 +40,19 @@ export class ListServiceTypeResolver extends BaseServiceTypeResolver< instancesStore, resolveServiceImplementation, requestedBy, + requestedPath, mode, descriptor, ), ); } else { services.push( - yield* resolveServiceImplementation(descriptor, requestedBy, mode), + yield* resolveServiceImplementation( + descriptor, + requestedBy, + requestedPath, + mode, + ), ); } } diff --git a/packages/@wroud/di/src/service-type-resolvers/OptionalServiceTypeResolver.ts b/packages/@wroud/di/src/service-type-resolvers/OptionalServiceTypeResolver.ts index 7dbc4c7..4231ef8 100644 --- a/packages/@wroud/di/src/service-type-resolvers/OptionalServiceTypeResolver.ts +++ b/packages/@wroud/di/src/service-type-resolvers/OptionalServiceTypeResolver.ts @@ -10,6 +10,7 @@ import type { IServiceCollection, IResolverServiceType, IServiceInstancesStore, + RequestPath, } from "../types/index.js"; import { BaseServiceTypeResolver, @@ -33,46 +34,48 @@ export class OptionalServiceTypeResolver extends BaseServiceTypeResolver< collection: IServiceCollection, instancesStore: IServiceInstancesStore, resolveServiceImplementation: IServiceDescriptorResolver, - requestedBy: Set>, + requestedBy: IServiceDescriptor | null, + requestedPath: RequestPath, mode: "sync" | "async", descriptor?: IServiceDescriptor, ): Generator, IOptionalService, unknown> { let next = this.next; function validateConstructorRequest() { - const lastRequestedBy = [...requestedBy].pop(); if ( - lastRequestedBy && - instancesStore.getInstanceInfo(lastRequestedBy)?.initialized === false + requestedBy && + instancesStore.getInstanceInfo(requestedBy)?.initialized === false ) { throw new Error( Debug.errors.optionalServiceAsDependency( getNameOfServiceType(next), - getNameOfDescriptor(lastRequestedBy), + getNameOfDescriptor(requestedBy), ), ); } } if (isServiceTypeResolver(next)) { - return new OptionalResolver(function* resolver(mode) { + return new OptionalResolver(function resolver(mode) { validateConstructorRequest(); - return yield* next.resolve( + return next.resolve( collection, instancesStore, resolveServiceImplementation, requestedBy, + requestedPath, mode, descriptor, ); }); } - return new OptionalResolver(function* resolver(mode) { + return new OptionalResolver(function resolver(mode) { validateConstructorRequest(); - return yield* resolveServiceImplementation( + return resolveServiceImplementation( descriptor ?? collection.getDescriptor(next), requestedBy, + requestedPath, mode, ); }); diff --git a/packages/@wroud/di/src/service-type-resolvers/SingleServiceTypeResolver.ts b/packages/@wroud/di/src/service-type-resolvers/SingleServiceTypeResolver.ts index 824bc37..a5ee387 100644 --- a/packages/@wroud/di/src/service-type-resolvers/SingleServiceTypeResolver.ts +++ b/packages/@wroud/di/src/service-type-resolvers/SingleServiceTypeResolver.ts @@ -4,6 +4,7 @@ import type { IServiceDescriptorResolver, IServiceCollection, IServiceInstancesStore, + RequestPath, } from "../types/index.js"; import { BaseServiceTypeResolver, @@ -18,30 +19,31 @@ export class SingleServiceTypeResolver extends BaseServiceTypeResolver< super(next); } - override *resolve( + override resolve( collection: IServiceCollection, instancesStore: IServiceInstancesStore, resolveServiceImplementation: IServiceDescriptorResolver, - requestedBy: Set>, + requestedBy: IServiceDescriptor | null, + requestedPath: RequestPath, mode: "sync" | "async", descriptor?: IServiceDescriptor, ): Generator, T, unknown> { - let next = this.next; - - if (isServiceTypeResolver(next)) { - return yield* next.resolve( + if (isServiceTypeResolver(this.next)) { + return this.next.resolve( collection, instancesStore, resolveServiceImplementation, requestedBy, + requestedPath, mode, descriptor, ); } - return yield* resolveServiceImplementation( - descriptor ?? collection.getDescriptor(next), + return resolveServiceImplementation( + descriptor ?? collection.getDescriptor(this.next), requestedBy, + requestedPath, mode, ); } diff --git a/packages/@wroud/di/src/types/IRequestPathNode.ts b/packages/@wroud/di/src/types/IRequestPathNode.ts new file mode 100644 index 0000000..4fe9189 --- /dev/null +++ b/packages/@wroud/di/src/types/IRequestPathNode.ts @@ -0,0 +1,4 @@ +export interface IRequestPathNode { + value: T; + next: IRequestPathNode | null; +} diff --git a/packages/@wroud/di/src/types/IResolverServiceType.ts b/packages/@wroud/di/src/types/IResolverServiceType.ts index f807d5b..d6b4647 100644 --- a/packages/@wroud/di/src/types/IResolverServiceType.ts +++ b/packages/@wroud/di/src/types/IResolverServiceType.ts @@ -2,6 +2,7 @@ import type { IServiceCollection } from "./IServiceCollection.js"; import type { IServiceDescriptor } from "./IServiceDescriptor.js"; import type { IServiceDescriptorResolver } from "./IServiceDescriptorResolver.js"; import type { IServiceInstancesStore } from "./IServiceInstancesStore.js"; +import type { RequestPath } from "./RequestPath.js"; import type { ServiceType } from "./ServiceType.js"; import type { SingleServiceType } from "./SingleServiceType.js"; @@ -13,7 +14,8 @@ export interface IResolverServiceType { collection: IServiceCollection, instancesStore: IServiceInstancesStore, resolveService: IServiceDescriptorResolver, - requestedBy: Set>, + requestedBy: IServiceDescriptor | null, + requestedPath: RequestPath, mode: "sync" | "async", descriptor?: IServiceDescriptor, ): Generator, Out, unknown>; diff --git a/packages/@wroud/di/src/types/IServiceCollection.ts b/packages/@wroud/di/src/types/IServiceCollection.ts index 4131b70..c820f9e 100644 --- a/packages/@wroud/di/src/types/IServiceCollection.ts +++ b/packages/@wroud/di/src/types/IServiceCollection.ts @@ -8,7 +8,9 @@ import type { SingleServiceType } from "./SingleServiceType.js"; export interface IServiceCollection extends Iterable> { getDescriptor(service: SingleServiceType): IServiceDescriptor; - getDescriptors(service: SingleServiceType): IServiceDescriptor[]; + getDescriptors( + service: SingleServiceType, + ): readonly IServiceDescriptor[]; addScoped(service: SingleServiceImplementation): this; addScoped( diff --git a/packages/@wroud/di/src/types/IServiceCollectionElement.ts b/packages/@wroud/di/src/types/IServiceCollectionElement.ts new file mode 100644 index 0000000..5017193 --- /dev/null +++ b/packages/@wroud/di/src/types/IServiceCollectionElement.ts @@ -0,0 +1,6 @@ +import type { IServiceDescriptor } from "./IServiceDescriptor.js"; + +export interface IServiceCollectionElement { + single: IServiceDescriptor; + all: IServiceDescriptor[]; +} diff --git a/packages/@wroud/di/src/types/IServiceDescriptor.ts b/packages/@wroud/di/src/types/IServiceDescriptor.ts index 2cf3447..f24f861 100644 --- a/packages/@wroud/di/src/types/IServiceDescriptor.ts +++ b/packages/@wroud/di/src/types/IServiceDescriptor.ts @@ -3,8 +3,7 @@ import type { SingleServiceType } from "./SingleServiceType.js"; import type { IServiceImplementationResolver } from "./IServiceImplementationResolver.js"; export interface IServiceDescriptor { - lifetime: ServiceLifetime; - service: SingleServiceType; - resolver: IServiceImplementationResolver; - dry?: boolean; + readonly lifetime: ServiceLifetime; + readonly service: SingleServiceType; + readonly resolver: IServiceImplementationResolver; } diff --git a/packages/@wroud/di/src/types/IServiceDescriptorResolver.ts b/packages/@wroud/di/src/types/IServiceDescriptorResolver.ts index f751182..a62388c 100644 --- a/packages/@wroud/di/src/types/IServiceDescriptorResolver.ts +++ b/packages/@wroud/di/src/types/IServiceDescriptorResolver.ts @@ -1,9 +1,11 @@ import type { IServiceDescriptor } from "./IServiceDescriptor.js"; +import type { RequestPath } from "./RequestPath.js"; export interface IServiceDescriptorResolver { ( descriptor: IServiceDescriptor, - requestedBy: Set>, + requestedBy: IServiceDescriptor | null, + requestedPath: RequestPath, mode: "sync" | "async", ): Generator, T, unknown>; } diff --git a/packages/@wroud/di/src/types/IServiceImplementationResolver.ts b/packages/@wroud/di/src/types/IServiceImplementationResolver.ts index 0aa0532..34b7d88 100644 --- a/packages/@wroud/di/src/types/IServiceImplementationResolver.ts +++ b/packages/@wroud/di/src/types/IServiceImplementationResolver.ts @@ -1,13 +1,15 @@ import type { IResolvedServiceImplementation } from "./IResolvedServiceImplementation.js"; import type { IServiceDescriptor } from "./IServiceDescriptor.js"; import type { IServiceTypeResolver } from "./IServiceTypeResolver.js"; +import type { RequestPath } from "./RequestPath.js"; export interface IServiceImplementationResolver { get name(): string; resolve( resolveService: IServiceTypeResolver, descriptor: IServiceDescriptor, - requestedBy: Set>, + requestedBy: IServiceDescriptor | null, + requestedPath: RequestPath, mode: "sync" | "async", ): Generator, IResolvedServiceImplementation, unknown>; } diff --git a/packages/@wroud/di/src/types/IServiceInstanceInfo.ts b/packages/@wroud/di/src/types/IServiceInstanceInfo.ts index 1210931..428dba3 100644 --- a/packages/@wroud/di/src/types/IServiceInstanceInfo.ts +++ b/packages/@wroud/di/src/types/IServiceInstanceInfo.ts @@ -3,10 +3,11 @@ import type { IServiceDescriptor } from "./IServiceDescriptor.js"; export interface IServiceInstanceInfo { descriptor: IServiceDescriptor; instance: T; - dependents: Set>; + dependents: IServiceInstanceInfo[]; initialized: boolean; disposed: boolean; - initialize(creator: () => T): void; + getInstance(): Generator; + initialize(creator: () => T): T; addDependent(dependent: IServiceInstanceInfo): void; disposeSync(): void; disposeAsync(): Promise; diff --git a/packages/@wroud/di/src/types/IServiceInstancesStore.ts b/packages/@wroud/di/src/types/IServiceInstancesStore.ts index 1555eee..605a742 100644 --- a/packages/@wroud/di/src/types/IServiceInstancesStore.ts +++ b/packages/@wroud/di/src/types/IServiceInstancesStore.ts @@ -9,7 +9,7 @@ export interface IServiceInstancesStore ): IServiceInstanceInfo | undefined; addInstance( descriptor: IServiceDescriptor, - requestedBy?: IServiceDescriptor, + requestedBy: IServiceDescriptor | null, ): IServiceInstanceInfo; [Symbol.dispose](): void; [Symbol.asyncDispose](): Promise; diff --git a/packages/@wroud/di/src/types/IServiceTypeResolver.ts b/packages/@wroud/di/src/types/IServiceTypeResolver.ts index 0cc7420..fe917a9 100644 --- a/packages/@wroud/di/src/types/IServiceTypeResolver.ts +++ b/packages/@wroud/di/src/types/IServiceTypeResolver.ts @@ -1,10 +1,12 @@ import type { IResolverServiceType } from "./IResolverServiceType.js"; import type { IServiceDescriptor } from "./IServiceDescriptor.js"; +import type { RequestPath } from "./RequestPath.js"; export interface IServiceTypeResolver { ( service: IResolverServiceType, - requestedBy: Set>, + requestedBy: IServiceDescriptor | null, + requestedPath: RequestPath, mode: "sync" | "async", ): Generator, T, unknown>; } diff --git a/packages/@wroud/di/src/types/RequestPath.ts b/packages/@wroud/di/src/types/RequestPath.ts new file mode 100644 index 0000000..0d8f7f5 --- /dev/null +++ b/packages/@wroud/di/src/types/RequestPath.ts @@ -0,0 +1,4 @@ +import type { IRequestPathNode } from "./IRequestPathNode.js"; +import type { IServiceDescriptor } from "./IServiceDescriptor.js"; + +export type RequestPath = IRequestPathNode | null>; diff --git a/packages/@wroud/di/src/types/index.ts b/packages/@wroud/di/src/types/index.ts index 8479667..29e52cc 100644 --- a/packages/@wroud/di/src/types/index.ts +++ b/packages/@wroud/di/src/types/index.ts @@ -3,6 +3,7 @@ export * from "./IAsyncServiceScope.js"; export * from "./IResolvedServiceImplementation.js"; export * from "./IResolverServiceType.js"; export * from "./IServiceCollection.js"; +export * from "./IServiceCollectionElement.js"; export * from "./IServiceConstructor.js"; export * from "./IServiceDescriptor.js"; export * from "./IServiceDescriptorResolver.js"; @@ -16,6 +17,8 @@ export * from "./IServiceTypeResolver.js"; export * from "./IServiceScope.js"; export * from "./IResolvedServiceImplementation.js"; export * from "./MapToServicesType.js"; +export * from "./RequestPath.js"; +export * from "./IRequestPathNode.js"; export * from "./ServiceImplementation.js"; export * from "./ServiceType.js"; export * from "./SingleServiceType.js"; diff --git a/packages/@wroud/tests-runner/bin/benchmark.mjs b/packages/@wroud/tests-runner/bin/benchmark.mjs new file mode 100644 index 0000000..3756632 --- /dev/null +++ b/packages/@wroud/tests-runner/bin/benchmark.mjs @@ -0,0 +1,16 @@ +#!/usr/bin/env node + +import v8 from "node:v8"; +import path from "node:path"; +import { startVitest, parseCLI } from "vitest/node"; + +v8.setFlagsFromString("--expose-gc"); +process.env["NODE_ENV"] = "production"; +const { filter, options } = parseCLI(["vitest", ...process.argv.slice(2)]); +const vitest = await startVitest("benchmark", filter, { + config: path.resolve(import.meta.dirname, "../vitest.config.ts"), + ...options, + mode: "production", +}); + +await vitest?.close(); diff --git a/packages/@wroud/tests-runner/package.json b/packages/@wroud/tests-runner/package.json index 15e62bc..684c84a 100644 --- a/packages/@wroud/tests-runner/package.json +++ b/packages/@wroud/tests-runner/package.json @@ -3,7 +3,10 @@ "private": true, "type": "module", "packageManager": "yarn@4.5.0", - "bin": "./bin/tests-runner.mjs", + "bin": { + "benchmark": "./bin/benchmark.mjs", + "tests-runner": "./bin/tests-runner.mjs" + }, "devDependencies": { "@tsconfig/node20": "^20", "@tsconfig/strictest": "^2", diff --git a/packages/@wroud/tests-runner/vitest.config.ts b/packages/@wroud/tests-runner/vitest.config.ts index 6d394fc..d5e8d1e 100644 --- a/packages/@wroud/tests-runner/vitest.config.ts +++ b/packages/@wroud/tests-runner/vitest.config.ts @@ -16,6 +16,11 @@ export default defineConfig({ setupFiles: [import.meta.resolve("./vitest.setup.ts")], include: ["**/lib/**/*.test.js"], + + benchmark: { + include: ["**/lib/**/*.bench.js"], + }, + poolOptions: { forks: { execArgv: ["--expose-gc"], diff --git a/yarn.lock b/yarn.lock index 4e23984..fee0918 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1854,6 +1854,32 @@ __metadata: languageName: node linkType: hard +"@inversifyjs/common@npm:1.3.3": + version: 1.3.3 + resolution: "@inversifyjs/common@npm:1.3.3" + checksum: 10c0/8024bd48a62f7b121eb68ce2f24b65b17ea6cba90836945686b30667a2e53d76f4032e67d063edc08194f64d22487814fb2d89de3b9e43b6d6dfa5304507fbd1 + languageName: node + linkType: hard + +"@inversifyjs/core@npm:1.3.4": + version: 1.3.4 + resolution: "@inversifyjs/core@npm:1.3.4" + dependencies: + "@inversifyjs/common": "npm:1.3.3" + "@inversifyjs/reflect-metadata-utils": "npm:0.2.3" + checksum: 10c0/4a357092d2387742990a1bf7d41c299365bc4431c12854b0cca4a6ae9c86c958eb464c7e83e6a6564242f155d9cbf626739150c118883a3cd7b28f9f0c30f2e5 + languageName: node + linkType: hard + +"@inversifyjs/reflect-metadata-utils@npm:0.2.3": + version: 0.2.3 + resolution: "@inversifyjs/reflect-metadata-utils@npm:0.2.3" + peerDependencies: + reflect-metadata: 0.2.2 + checksum: 10c0/372f640ef48ed65652fd1a7d672636af223f7177f72af99017830a9ce5bdd3813ad1764e61b75391164ddc34bb8c4f18fc6856f120ff0e5e8fa74ad969b1179c + languageName: node + linkType: hard + "@isaacs/cliui@npm:^8.0.2": version: 8.0.2 resolution: "@isaacs/cliui@npm:8.0.2" @@ -4108,6 +4134,24 @@ __metadata: languageName: unknown linkType: soft +"@wroud/di-tools-benchmark@workspace:packages/@wroud/di-tools-benchmark": + version: 0.0.0-use.local + resolution: "@wroud/di-tools-benchmark@workspace:packages/@wroud/di-tools-benchmark" + dependencies: + "@wroud/di": "workspace:^" + "@wroud/tests-runner": "workspace:^" + "@wroud/tsconfig": "workspace:^" + brandi: "npm:^5" + inversify: "npm:^6" + reflect-metadata: "npm:^0" + rimraf: "npm:^6" + tslib: "npm:^2" + tsyringe: "npm:^4" + typescript: "npm:^5" + vitest: "npm:^2" + languageName: unknown + linkType: soft + "@wroud/di-tools-codemod@workspace:*, @wroud/di-tools-codemod@workspace:packages/@wroud/di-tools-codemod": version: 0.0.0-use.local resolution: "@wroud/di-tools-codemod@workspace:packages/@wroud/di-tools-codemod" @@ -4126,7 +4170,7 @@ __metadata: languageName: unknown linkType: soft -"@wroud/di@workspace:*, @wroud/di@workspace:^0, @wroud/di@workspace:packages/@wroud/di": +"@wroud/di@workspace:*, @wroud/di@workspace:^, @wroud/di@workspace:^0, @wroud/di@workspace:packages/@wroud/di": version: 0.0.0-use.local resolution: "@wroud/di@workspace:packages/@wroud/di" dependencies: @@ -4230,6 +4274,7 @@ __metadata: vite: "npm:^5" vitest: "npm:^2" bin: + benchmark: ./bin/benchmark.mjs tests-runner: ./bin/tests-runner.mjs languageName: unknown linkType: soft @@ -5293,6 +5338,13 @@ __metadata: languageName: node linkType: hard +"brandi@npm:^5": + version: 5.0.0 + resolution: "brandi@npm:5.0.0" + checksum: 10c0/3d8428b63465140b01e696ee9336cf47c6676d2264c70f80bf7f316c0a06bafc6552075bc88a4d5ae99a1b3e39f75cae4e4d5e8a3cb1367057e0cf31476b9bc8 + languageName: node + linkType: hard + "browserslist@npm:^4.24.0": version: 4.24.0 resolution: "browserslist@npm:4.24.0" @@ -7903,6 +7955,16 @@ __metadata: languageName: node linkType: hard +"inversify@npm:^6": + version: 6.1.4 + resolution: "inversify@npm:6.1.4" + dependencies: + "@inversifyjs/common": "npm:1.3.3" + "@inversifyjs/core": "npm:1.3.4" + checksum: 10c0/07123b3ffd77639ae7613207c96e7338b25eb883815b53f18d2146c6a09715493a9d3ffc0c42bb9a1218c51337f86ceac758e7ad4c1cd64c9a7713b5c2d0447a + languageName: node + linkType: hard + "ip-address@npm:^9.0.5": version: 9.0.5 resolution: "ip-address@npm:9.0.5" @@ -10191,6 +10253,13 @@ __metadata: languageName: node linkType: hard +"reflect-metadata@npm:^0": + version: 0.2.2 + resolution: "reflect-metadata@npm:0.2.2" + checksum: 10c0/1cd93a15ea291e420204955544637c264c216e7aac527470e393d54b4bb075f10a17e60d8168ec96600c7e0b9fcc0cb0bb6e91c3fbf5b0d8c9056f04e6ac1ec2 + languageName: node + linkType: hard + "regenerator-runtime@npm:^0.14.0": version: 0.14.1 resolution: "regenerator-runtime@npm:0.14.1" @@ -11270,6 +11339,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:^1.9.3": + version: 1.14.1 + resolution: "tslib@npm:1.14.1" + checksum: 10c0/69ae09c49eea644bc5ebe1bca4fa4cc2c82b7b3e02f43b84bd891504edf66dbc6b2ec0eef31a957042de2269139e4acff911e6d186a258fb14069cd7f6febce2 + languageName: node + linkType: hard + "tslib@npm:^2, tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.4.0": version: 2.8.0 resolution: "tslib@npm:2.8.0" @@ -11300,6 +11376,15 @@ __metadata: languageName: node linkType: hard +"tsyringe@npm:^4": + version: 4.8.0 + resolution: "tsyringe@npm:4.8.0" + dependencies: + tslib: "npm:^1.9.3" + checksum: 10c0/e13810e8ff39c4093acd0649bc5db3c164825827631e1522cd9d5ca8694a018447fa1c24f059ea54e93b1020767b1131b9dc9ce598dabfc9aa41c11544bbfe19 + languageName: node + linkType: hard + "tunnel@npm:^0.0.6": version: 0.0.6 resolution: "tunnel@npm:0.0.6"