Skip to content

Commit

Permalink
refactor: remove support of inline config and provider
Browse files Browse the repository at this point in the history
  • Loading branch information
exuanbo committed Oct 15, 2024
1 parent 7ea7ffa commit 78667c4
Show file tree
Hide file tree
Showing 13 changed files with 162 additions and 256 deletions.
24 changes: 0 additions & 24 deletions src/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,30 +94,6 @@ describe("Features", () => {
expect(a).toBeInstanceOf(AImpl);
});

it("should override config", () => {
const container = new Container();

@Scoped(InjectionScope.Transient)
class A {}

class B {
@Inject({
token: A,
scope: InjectionScope.Container,
})
a!: A;
}

const b = container.resolve(B);
expect(container.unsafe_instanceCache.has(A)).toBe(true);

const a = container.resolve({
token: A,
scope: InjectionScope.Container,
});
expect(a).toBe(b.a);
});

it("should resolve multiple tokens", () => {
const container = new Container();

Expand Down
31 changes: 1 addition & 30 deletions src/__tests__/inject.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {afterEach, beforeEach, describe, expect, it, vi} from "vitest";

import {Container, inject, InjectionScope, Type} from "..";
import {Container, inject, Type} from "..";

describe("inject", () => {
beforeEach(() => {
Expand Down Expand Up @@ -38,24 +38,6 @@ describe("inject", () => {
expect(a.value).toBeInstanceOf(B);
});

it("should inject with inline config", () => {
const container = new Container();

class A {
value = inject({
token: B,
scope: InjectionScope.Container,
});
}

class B {}

const a = container.resolve(A);
expect(a).toBeInstanceOf(A);
expect(a.value).toBeInstanceOf(B);
expect(container.unsafe_instanceCache.get(B)).toBe(a.value);
});

it("should error if outside context", () => {
const container = new Container();

Expand Down Expand Up @@ -97,15 +79,4 @@ describe("inject", () => {
const a = container.resolve(A);
expect(a.value).toBeInstanceOf(BImpl);
});

it("should inject container", () => {
const container = new Container();

class A {
container = inject(Container);
}

const a = container.resolve(A);
expect(a.container).toBe(container);
});
});
19 changes: 0 additions & 19 deletions src/config.ts

This file was deleted.

153 changes: 40 additions & 113 deletions src/container.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,31 @@
import {type InjectionConfig, isConfigLike} from "./config";
import {assert, ErrorMessage, expectNever} from "./errors";
import type {Injection, Injections} from "./injection";
import {useInjectionContext, withInjectionContext} from "./injection-context";
import {getMetadata} from "./metadata";
import {
type InjectionProvider,
isClassProvider,
isFactoryProvider,
isProvider,
isValueProvider,
NullProvider,
UndefinedProvider,
} from "./provider";
import {type Registration, Registry} from "./registry";
import {InjectionScope} from "./scope";
import {type Constructor, type InjectionToken, isConstructor, Type} from "./token";
import {type Constructor, type InjectionToken, type InjectionTokens, isConstructor} from "./token";
import {Stack} from "./utils/stack";

const ProviderRegistry: typeof Map<InjectionToken, InjectionProvider> = Map;

const InstanceCache: typeof Map<InjectionToken, any> = Map;

export interface ContainerOptions {
parent?: Container;
defaultScope?: InjectionScope;
}

export class Container {
#reservedRegistry = new ProviderRegistry([
[Type.Any, null!],
[Type.Null, NullProvider],
[Type.Undefined, UndefinedProvider],
]);

#providerRegistry = new ProviderRegistry();

get unsafe_providerRegistry(): InstanceType<typeof ProviderRegistry> {
return this.#providerRegistry;
}
readonly registry: Registry;

#instanceCache = new InstanceCache();

get unsafe_instanceCache(): InstanceType<typeof InstanceCache> {
return this.#instanceCache;
}

parent?: Container;
defaultScope: InjectionScope;

constructor(options?: ContainerOptions);
constructor({parent, defaultScope = InjectionScope.Inherited}: ContainerOptions = {}) {
this.parent = parent;
this.registry = new Registry(parent?.registry);
this.defaultScope = defaultScope;
this.#reservedRegistry.set(Container, {token: Container, useValue: this});
}

createChild(): Container {
Expand All @@ -62,31 +36,17 @@ export class Container {
}

clearCache(): void {
this.#instanceCache.clear();
for (const registration of this.registry.values()) {
registration.cache &&= undefined;
}
}

resetRegistry(): void {
this.#instanceCache.clear();
this.#providerRegistry.clear();
this.registry.clear();
}

isRegistered<Value>(token: InjectionToken<Value>): boolean {
return (
this.#providerRegistry.has(token)
|| !!(this.parent?.isRegistered(token))
);
}

#getProvider<Value>(token: InjectionToken<Value>) {
return (
this.#reservedRegistry.get(token)
|| this.#providerRegistry.get(token)
);
}

#setProvider<Value>(token: InjectionToken<Value>, provider: InjectionProvider<Value>) {
assert(!this.#reservedRegistry.has(token), ErrorMessage.ReservedToken, token.name);
this.#providerRegistry.set(token, provider);
return this.registry.has(token);
}

register<Instance extends object>(Class: Constructor<Instance>): void;
Expand All @@ -102,83 +62,48 @@ export class Container {
useClass: Class,
scope: metadata?.scope,
};
this.#setProvider(token, provider);
this.registry.set(token, {provider});
});
}
else {
const provider = providable;
const token = provider.token;
this.#setProvider(token, provider);
const {token} = provider;
this.registry.set(token, {provider});
}
}

resolve<Values extends unknown[]>(...injections: Injections<Values>): Values[number];
resolve<Value>(...injections: Injection<Value>[]): Value {
for (const injection of injections) {
if (isConfigLike(injection)) {
if (isProvider(injection)) {
const provider = injection;
return this.resolveValue(provider);
}
const config = injection;
const token = config.token;
const provider = this.resolveProvider(token);
if (provider) {
const scope = config.scope;
return this.resolveValue({...provider, ...(scope && {scope})});
}
resolve<Values extends unknown[]>(...tokens: InjectionTokens<Values>): Values[number];
resolve<Value>(...tokens: InjectionToken<Value>[]): Value {
for (const token of tokens) {
const registration = this.registry.get(token);
if (registration) {
return this.#resolveValue(registration);
}
else {
const token = injection;
const provider = this.resolveProvider(token);
if (provider) {
return this.resolveValue(provider);
}
if (isConstructor(token)) {
const Class = token;
const metadata = getMetadata(Class);
const provider = {
token,
useClass: Class,
scope: metadata?.scope,
};
return this.#resolveValue({provider});
}
}
const tokenNames = injections.map((injection) => {
if (isConfigLike(injection)) {
const config = injection;
const token = config.token;
return token.name;
}
const token = injection;
return token.name;
});
const formatter = new Intl.ListFormat("en", {style: "narrow", type: "conjunction"});
const tokenNames = tokens.map((token) => token.name);
const formatter = new Intl.ListFormat("en", {style: "narrow"});
assert(false, ErrorMessage.UnresolvableToken, formatter.format(tokenNames));
}

resolveProvider<Value>(token: InjectionToken<Value>): InjectionProvider<Value> | undefined {
if (isConstructor(token)) {
const Class = token;
const provider = this.#getProvider(token);
if (provider) {
return provider;
}
const metadata = getMetadata(Class);
return {
token,
useClass: Class,
scope: metadata?.scope,
};
}
else {
const provider = this.#getProvider(token);
if (provider) {
return provider;
}
}
}

resolveValue<Value>(provider: InjectionProvider<Value>): Value {
#resolveValue<Value>(registration: Registration<Value>): Value {
const {provider} = registration;
if (isClassProvider(provider)) {
const Class = provider.useClass;
return this.#resolveScopedInstance(provider, () => new Class());
return this.#resolveScopedInstance(registration, () => new Class());
}
else if (isFactoryProvider(provider)) {
const factory = provider.useFactory;
return this.#resolveScopedInstance(provider, factory);
return this.#resolveScopedInstance(registration, factory);
}
else if (isValueProvider(provider)) {
const value = provider.useValue;
Expand All @@ -187,7 +112,7 @@ export class Container {
expectNever(provider);
}

#resolveScopedInstance<T>({token, scope = this.defaultScope}: InjectionConfig<T>, instantiate: () => T): T {
#resolveScopedInstance<T>(registration: Registration<T>, instantiate: () => T): T {
const context = useInjectionContext();

if (!context || context.container != this) {
Expand All @@ -198,9 +123,11 @@ export class Container {
instances: new Map(),
dependents: new Map(),
},
}, () => this.#resolveScopedInstance({token, scope}, instantiate));
}, () => this.#resolveScopedInstance(registration, instantiate));
}

const {token, scope = this.defaultScope} = registration.provider;

const resolution = context.resolution;

if (resolution.stack.has(token)) {
Expand All @@ -219,11 +146,11 @@ export class Container {
resolution.stack.push(token, {token, scope: resolvedScope});
try {
if (resolvedScope == InjectionScope.Container) {
if (this.#instanceCache.has(token)) {
return this.#instanceCache.get(token);
if (registration.cache) {
return registration.cache.current;
}
const instance = instantiate();
this.#instanceCache.set(token, instance);
registration.cache = {current: instance};
return instance;
}
else if (resolvedScope == InjectionScope.Resolution) {
Expand Down
9 changes: 4 additions & 5 deletions src/decorators.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import {inject} from "./inject";
import type {Injection, Injections} from "./injection";
import {metadataRegistry} from "./metadata";
import type {InjectionScope} from "./scope";
import type {Constructor, InjectionToken} from "./token";
import type {Constructor, InjectionToken, InjectionTokens} from "./token";

export type ClassDecorator<Class extends Constructor<object>> = (
value: Class,
Expand Down Expand Up @@ -33,10 +32,10 @@ export function Scoped<This extends object>(scope: InjectionScope): ClassDecorat
};
}

export function Inject<Values extends unknown[]>(...injections: Injections<Values>): ClassFieldDecorator<Values[number]>;
export function Inject<Value>(...injections: Injection<Value>[]): ClassFieldDecorator<Value> {
export function Inject<Values extends unknown[]>(...tokens: InjectionTokens<Values>): ClassFieldDecorator<Values[number]>;
export function Inject<Value>(...tokens: InjectionToken<Value>[]): ClassFieldDecorator<Value> {
return (_value, _context) =>
function (this, _initialValue) {
return inject.by(this, ...injections);
return inject.by(this, ...tokens);
};
}
2 changes: 2 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ export const ErrorMessage = {
InjectOutsideOfContext: "inject outside of context",
} as const;

// @internal
export function assert(condition: unknown, ...args: any[]): asserts condition {
if (!condition) {
const formatter = new Intl.ListFormat("en", {style: "narrow", type: "unit"});
throw new Error(formatter.format(args));
}
}

// @internal
export function expectNever(value: never): never {
throw new TypeError("unexpected value: " + value);
}
Loading

0 comments on commit 78667c4

Please sign in to comment.