Skip to content

Commit

Permalink
fix(di): add global flag to register correctly provider on GlobalRegi…
Browse files Browse the repository at this point in the history
…stry vs injector.container
  • Loading branch information
Romakita committed Nov 25, 2024
1 parent 79ebad7 commit 81d1c9c
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 105 deletions.
34 changes: 13 additions & 21 deletions packages/di/src/common/decorators/inject.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,14 @@ describe("@Inject()", () => {
test: InjectorService;
}

const inj = injector({rebuild: true});
const instance = await inj.invoke<Test>(Test);
const instance = inject<Test>(Test);

expect(instance).toBeInstanceOf(Test);
expect(instance.test).toBeInstanceOf(InjectorService);
});
it("should inject service and async factory", async () => {
// const inj = injector({rebuild: true});

// GIVEN
class Test {
constructor(public type: string) {}
Expand Down Expand Up @@ -69,12 +70,10 @@ describe("@Inject()", () => {
test: Test;
}

const inj = injector({rebuild: true});

await inj.load();
await injector().load();

const parent1 = await inj.invoke<Parent1>(Parent1);
const parent2 = await inj.invoke<Parent2>(Parent2);
const parent1 = inject<Parent1>(Parent1);
const parent2 = inject<Parent2>(Parent2);

expect(parent1.test).toBeInstanceOf(Test);
expect(parent2.test).toBeInstanceOf(Test);
Expand All @@ -87,8 +86,7 @@ describe("@Inject()", () => {
test: InjectorService;
}

const inj = injector({rebuild: true});
const instance = await inj.invoke<Test>(Test);
const instance = inject(Test);

expect(instance).toBeInstanceOf(Test);
expect(instance.test).toBeInstanceOf(InjectorService);
Expand All @@ -101,8 +99,7 @@ describe("@Inject()", () => {
test: InjectorService;
}

const inj = injector({rebuild: true});
const instance = await inj.invoke<Test>(Test);
const instance = inject<Test>(Test);

expect(instance).toBeInstanceOf(Test);
expect(instance.test).toBeInstanceOf(InjectorService);
Expand Down Expand Up @@ -151,11 +148,9 @@ describe("@Inject()", () => {
instances: InterfaceGroup[];
}

const inj = injector({rebuild: true});
await injector().load();

await inj.load();

const instance = await inj.invoke<MyInjectable>(MyInjectable);
const instance = inject<MyInjectable>(MyInjectable);

expect(instance.instances).toBeInstanceOf(Array);
expect(instance.instances).toHaveLength(3);
Expand Down Expand Up @@ -212,8 +207,7 @@ describe("@Inject()", () => {
constructor(@Inject(InjectorService) readonly injector: InjectorService) {}
}

const inj = injector({rebuild: true});
const instance = await inj.invoke<MyInjectable>(MyInjectable);
const instance = inject(MyInjectable);

expect(instance.injector).toBeInstanceOf(InjectorService);
});
Expand Down Expand Up @@ -263,11 +257,9 @@ describe("@Inject()", () => {
constructor(@Inject(TOKEN_GROUPS) readonly instances: InterfaceGroup[]) {}
}

const inj = injector({rebuild: true});

await inj.load();
await injector().load();

const instance = await inj.invoke<MyInjectable>(MyInjectable);
const instance = inject(MyInjectable);

expect(instance.instances).toBeInstanceOf(Array);
expect(instance.instances).toHaveLength(3);
Expand Down
73 changes: 1 addition & 72 deletions packages/di/src/common/fn/injectable.ts
Original file line number Diff line number Diff line change
@@ -1,78 +1,7 @@
import "../registries/ProviderRegistry.js";

import {Store, type Type} from "@tsed/core";

import {ControllerProvider} from "../domain/ControllerProvider.js";
import type {Provider} from "../domain/Provider.js";
import {ProviderType} from "../domain/ProviderType.js";
import type {ProviderOpts} from "../interfaces/ProviderOpts.js";
import type {TokenProvider} from "../interfaces/TokenProvider.js";
import {GlobalProviders} from "../registries/GlobalProviders.js";

type ProviderBuilder<Token extends TokenProvider, BaseProvider, T extends object> = {
[K in keyof T as T[K] extends (...args: any[]) => any ? never : K]: (value: T[K]) => ProviderBuilder<Token, BaseProvider, T>;
} & {
inspect(): BaseProvider;
store(): Store;
token(): Token;
factory(f: (...args: unknown[]) => unknown): ProviderBuilder<Token, BaseProvider, T>;
asyncFactory(f: (...args: unknown[]) => Promise<unknown>): ProviderBuilder<Token, BaseProvider, T>;
value(v: unknown): ProviderBuilder<Token, BaseProvider, T>;
class(c: Type): ProviderBuilder<Token, BaseProvider, T>;
};

export function providerBuilder<Provider, Picked extends keyof Provider>(props: string[], baseOpts: Partial<ProviderOpts<Provider>> = {}) {
return <Token extends TokenProvider>(
token: Token,
options: Partial<ProviderOpts<Type>> = {}
): ProviderBuilder<Token, Provider, Pick<Provider, Picked>> => {
const provider = GlobalProviders.merge(token, {
...options,
...baseOpts,
provide: token
});

return props.reduce(
(acc, prop) => {
return {
...acc,
[prop]: function (value: any) {
(provider as any)[prop] = value;
return this;
}
};
},
{
factory(factory: any) {
provider.useFactory = factory;
return this;
},
asyncFactory(asyncFactory: any) {
provider.useAsyncFactory = asyncFactory;
return this;
},
value(value: any) {
provider.useValue = value;
provider.type = ProviderType.VALUE;
return this;
},
class(k: any) {
provider.useClass = k;
return this;
},
store() {
return provider.store;
},
inspect() {
return provider;
},
token() {
return provider.token as Token;
}
} as ProviderBuilder<Token, Provider, Pick<Provider, Picked>>
);
};
}
import {providerBuilder} from "../utils/providerBuilder.js";

type PickedProps = "scope" | "path" | "alias" | "hooks" | "deps" | "imports" | "configuration" | "priority";

Expand Down
20 changes: 12 additions & 8 deletions packages/di/src/common/registries/GlobalProviders.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import {getClassOrSymbol, Type} from "@tsed/core";

import type {LocalsContainer} from "../domain/LocalsContainer.js";
import {Provider} from "../domain/Provider.js";
import {ProviderType} from "../domain/ProviderType.js";
import {ProviderOpts} from "../interfaces/ProviderOpts.js";
import {RegistrySettings} from "../interfaces/RegistrySettings.js";
import {ResolvedInvokeOptions} from "../interfaces/ResolvedInvokeOptions.js";
import {TokenProvider} from "../interfaces/TokenProvider.js";
import type {InjectorService} from "../services/InjectorService.js";

export class GlobalProviderRegistry extends Map<TokenProvider, Provider> {
#settings: Map<TokenProvider, RegistrySettings> = new Map();
Expand Down Expand Up @@ -47,6 +44,10 @@ export class GlobalProviderRegistry extends Map<TokenProvider, Provider> {
* @param options
*/
merge(target: TokenProvider, options: Partial<ProviderOpts>) {
if (options.global === false) {
return GlobalProviders.createProvider(target, options);
}

const meta = this.createIfNotExists(target, options);

Object.keys(options).forEach((key) => {
Expand Down Expand Up @@ -99,18 +100,21 @@ export class GlobalProviderRegistry extends Map<TokenProvider, Provider> {
);
}

protected createProvider(key: TokenProvider, options: Partial<ProviderOpts<any>>) {
const type = options.type || ProviderType.PROVIDER;
const {model = Provider} = this.#settings.get(type) || {};

return new model(key, options);
}

/**
*
* @param key
* @param options
*/
protected createIfNotExists(key: TokenProvider, options: Partial<ProviderOpts>): Provider {
const type = options.type || ProviderType.PROVIDER;

if (!this.has(key)) {
const {model = Provider} = this.#settings.get(type) || {};

const item = new model(key, options);
const item = this.createProvider(key, options);

this.set(key, item);
}
Expand Down
3 changes: 2 additions & 1 deletion packages/di/src/common/registries/ProviderRegistry.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ describe("ProviderRegistry", () => {
registerProvider({provide: Test});

expect(GlobalProviders.merge).toHaveBeenCalledWith(Test, {
provide: Test
provide: Test,
global: true
});
});
});
Expand Down
7 changes: 4 additions & 3 deletions packages/di/src/common/registries/ProviderRegistry.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import {ControllerProvider} from "../domain/ControllerProvider.js";
import {ProviderType} from "../domain/ProviderType.js";
import {injectable} from "../fn/injectable.js";
import type {ProviderOpts} from "../interfaces/ProviderOpts.js";
import {GlobalProviders} from "./GlobalProviders.js";

GlobalProviders.createRegistry(ProviderType.CONTROLLER, ControllerProvider);

/**
* Register a provider configuration.
* @param {ProviderOpts<any>} provider
* @param {ProviderOpts<any>} opts
*/
export function registerProvider<Type = any>(provider: Partial<ProviderOpts<Type>> & Pick<ProviderOpts<Type>, "provide">) {
return GlobalProviders.merge(provider.provide, provider);
export function registerProvider<Type = any>(opts: Partial<ProviderOpts<Type>> & Pick<ProviderOpts<Type>, "provide">) {
return injectable(opts.provide, opts as unknown as Partial<ProviderOpts>).inspect();
}
9 changes: 9 additions & 0 deletions packages/di/src/common/services/InjectorService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export class InjectorService extends Container {
public logger: DILogger = console;
private resolvedConfiguration: boolean = false;
#cache = new LocalsContainer();
#loaded: boolean = false;

constructor() {
super();
Expand All @@ -59,6 +60,10 @@ export class InjectorService extends Container {
this.#cache.set(Configuration, settings);
}

isLoaded() {
return this.#loaded;
}

/**
* Return a list of instance build by the injector.
*/
Expand Down Expand Up @@ -248,6 +253,10 @@ export class InjectorService extends Container {
* @param container
*/
async load(container: Container = createContainer()) {
// avoid provider registration in the GlobalContainer during the loading phase
// using injectable() or providerBuilder()
this.#loaded = true;

await $asyncEmit("$beforeInit");

this.bootstrap(container);
Expand Down
81 changes: 81 additions & 0 deletions packages/di/src/common/utils/providerBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import "../registries/ProviderRegistry.js";

import {Store, type Type} from "@tsed/core";

import {ProviderType} from "../domain/ProviderType.js";
import {hasInjector, injector} from "../fn/injector.js";
import type {ProviderOpts} from "../interfaces/ProviderOpts.js";
import type {TokenProvider} from "../interfaces/TokenProvider.js";
import {GlobalProviders} from "../registries/GlobalProviders.js";

type ProviderBuilder<Token extends TokenProvider, BaseProvider, T extends object> = {
[K in keyof T as T[K] extends (...args: any[]) => any ? never : K]: (value: T[K]) => ProviderBuilder<Token, BaseProvider, T>;
} & {
inspect(): BaseProvider;
store(): Store;
token(): Token;
factory(f: (...args: unknown[]) => unknown): ProviderBuilder<Token, BaseProvider, T>;
asyncFactory(f: (...args: unknown[]) => Promise<unknown>): ProviderBuilder<Token, BaseProvider, T>;
value(v: unknown): ProviderBuilder<Token, BaseProvider, T>;
class(c: Type): ProviderBuilder<Token, BaseProvider, T>;
};

export function providerBuilder<Provider, Picked extends keyof Provider>(props: string[], baseOpts: Partial<ProviderOpts<Provider>> = {}) {
return <Token extends TokenProvider>(
token: Token,
options: Partial<ProviderOpts<Type>> = {}
): ProviderBuilder<Token, Provider, Pick<Provider, Picked>> => {
const merged = {
global: !hasInjector() || injector().isLoaded(),
...options,
...baseOpts,
provide: token
};

const provider = GlobalProviders.merge(token, merged);

if (!merged.global) {
injector().setProvider(token, provider);
}

return props.reduce(
(acc, prop) => {
return {
...acc,
[prop]: function (value: any) {
(provider as any)[prop] = value;
return this;
}
};
},
{
factory(factory: any) {
provider.useFactory = factory;
return this;
},
asyncFactory(asyncFactory: any) {
provider.useAsyncFactory = asyncFactory;
return this;
},
value(value: any) {
provider.useValue = value;
provider.type = ProviderType.VALUE;
return this;
},
class(k: any) {
provider.useClass = k;
return this;
},
store() {
return provider.store;
},
inspect() {
return provider;
},
token() {
return provider.token as Token;
}
} as ProviderBuilder<Token, Provider, Pick<Provider, Picked>>
);
};
}

0 comments on commit 81d1c9c

Please # to comment.