Skip to content

Commit

Permalink
refactoring WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
zheksoon committed Mar 30, 2024
1 parent f155b0c commit bad10c5
Show file tree
Hide file tree
Showing 11 changed files with 2,404 additions and 16 deletions.
725 changes: 720 additions & 5 deletions package-lock.json

Large diffs are not rendered by default.

13 changes: 6 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,16 @@
"test:coverage": "vitest --coverage"
},
"devDependencies": {
"@types/node": "^20.11.30",
"@vitest/coverage-v8": "^1.4.0",
"@vitest/ui": "latest",
"typescript": "^5.4.2",
"vite": "latest",
"vitest": "latest"
"vitest": "latest",
"vite-plugin-dts": "^3.8.1"
},
"types": "dist/dioma.d.ts",
"module": "dist/es6/dioma.js",
"main": "dist/common/dioma.js",
"umd:main": "dist/umd/dioma.js",
"types": "dist/index.d.ts",
"module": "dist/dioma.mjs",
"main": "dist/dioma.js",
"umd:main": "dist/dioma.umd.js",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
183 changes: 183 additions & 0 deletions src/container.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import type { ScopedClass, TokenDescriptor, ScopeHandler, IContainer } from "./types";
import { Scopes } from "./scopes";
import { Token } from "./token";
import {
AsyncCycleDependencyError,
CycleDependencyError,
TokenNotRegisteredError,
} from "./errors";

type TokenOrClass = Token<any> | ScopedClass;

type TokenDescriptorWithContainer = TokenDescriptor & {
container: Container;
};

const MAX_LOOP_COUNT = 100;

export class Container implements IContainer {
private instances = new WeakMap();

private resolutionContainer: Container | null = null;

private resolutionSet = new Set<TokenOrClass>();

private pendingPromiseMap = new Map<TokenOrClass, Promise<InstanceType<ScopedClass>>>();

private tokenDescriptorMap = new Map<TokenOrClass, TokenDescriptorWithContainer>();

constructor(private parentContainer: Container | null = null, public name?: string) {}

loopCounter = 0;

childContainer = (name?: string) => {
return new Container(this, name);
};

getInstance(cls: any, args: any[] = []) {
let instance = null;
let container: Container | null = this;

while (!instance && container) {
instance = container.instances.get(cls);
container = container.parentContainer;
}

if (!instance) {
instance = new cls(...args);

this.instances.set(cls, instance);
}

return instance;
}

private getTokenDescriptor(
clsOrToken: ScopedClass | Token<any>
): TokenDescriptorWithContainer | undefined {
let tokenDescriptor = this.tokenDescriptorMap.get(clsOrToken);

if (!tokenDescriptor && this.parentContainer) {
tokenDescriptor = this.parentContainer.getTokenDescriptor(clsOrToken);
}

return tokenDescriptor;
}

injectImpl<T extends ScopedClass, Args extends any[]>(
clsOrToken: T | Token<T>,
args: Args,
resolutionContainer = this.resolutionContainer
): InstanceType<T> {
this.resolutionContainer = resolutionContainer || new Container();

let cls = clsOrToken as ScopedClass;
let scope: ScopeHandler;
let container: Container | null = this;

const descriptor = this.getTokenDescriptor(clsOrToken);

if (descriptor) {
container = descriptor.container;
cls = descriptor.class;
scope = descriptor.scope || cls.scope || Scopes.Transient();
} else {
if (clsOrToken instanceof Token) {
throw new TokenNotRegisteredError();
}

scope = cls.scope || Scopes.Transient();
}

try {
if (this.resolutionSet.has(clsOrToken)) {
throw new CycleDependencyError();
}

this.resolutionSet.add(clsOrToken);

return scope(cls, args, container, this.resolutionContainer);
} finally {
this.resolutionSet.delete(clsOrToken);
this.resolutionContainer = resolutionContainer;

if (!resolutionContainer) {
this.loopCounter = 0;
}
}
}

inject = <T extends ScopedClass, Args extends any[]>(
cls: T | Token<T>,
...args: Args
) => {
return this.injectImpl(cls, args, undefined);
};

injectAsync = <T extends ScopedClass, Args extends any[]>(
cls: T | Token<T>,
...args: Args
): Promise<InstanceType<T>> => {
const resolutionContainer = this.resolutionContainer;

this.loopCounter += 1;

if (this.loopCounter > MAX_LOOP_COUNT) {
throw new AsyncCycleDependencyError();
}

if (this.pendingPromiseMap.has(cls)) {
return this.pendingPromiseMap.get(cls) as Promise<InstanceType<T>>;
}

if (this.instances.has(cls)) {
return Promise.resolve(this.instances.get(cls));
}

const promise = Promise.resolve().then(() => {
try {
return this.injectImpl(cls, args, resolutionContainer);
} finally {
this.pendingPromiseMap.delete(cls);
}
});

this.pendingPromiseMap.set(cls, promise);

return promise;
};

register = (tokenDescriptor: TokenDescriptor) => {
const token = tokenDescriptor.token || tokenDescriptor.class;

const descriptorWithContainer = { ...tokenDescriptor, container: this };

this.tokenDescriptorMap.set(token, descriptorWithContainer);
// this.tokenDescriptorMap.set(tokenDescriptor.class, descriptorWithContainer);
};

unregister = (token: Token<any> | ScopedClass) => {
const descriptor = this.getTokenDescriptor(token);

this.tokenDescriptorMap.delete(token);

if (descriptor) {
this.instances.delete(descriptor.class);
}
};

reset = () => {
this.instances = new WeakMap();
this.resolutionSet = new Set();
this.pendingPromiseMap = new Map();
this.resolutionContainer = null;
};
}

export const globalContainer = new Container(null, "Global container");

export const inject = globalContainer.inject;

export const injectAsync = globalContainer.injectAsync;

export const childContainer = globalContainer.childContainer;
23 changes: 23 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export class CycleDependencyError extends Error {
constructor() {
super("Circular dependency detected");
}
}

export class AsyncCycleDependencyError extends Error {
constructor() {
super("Circular dependency detected in async resolution");
}
}

export class ArgumentsError extends Error {
constructor(scope: string, className: string) {
super(`Arguments are not supported for ${scope} of ${className}`);
}
}

export class TokenNotRegisteredError extends Error {
constructor() {
super("Token is not registered in the container");
}
}
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from "./container";
export * from "./scopes";
export * from "./token";
export * from "./types";
export * from "./errors";
39 changes: 39 additions & 0 deletions src/scopes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { ArgumentsError } from "./errors";
import { globalContainer } from "./container";
import type { ScopeHandler } from "./types";

export class Scopes {
public static Singleton(): ScopeHandler {
return function SingletonScope(cls, args) {
if (args.length > 0) {
throw new ArgumentsError(SingletonScope.name, cls.name);
}

return globalContainer.getInstance(cls);
};
}

public static Transient(): ScopeHandler {
return function TransientScope(cls, args) {
return new cls(...args);
};
}

public static Container(): ScopeHandler {
return function ContainerScope(cls, args, container) {
if (args.length > 0) {
throw new ArgumentsError(ContainerScope.name, cls.name);
}

return container.getInstance(cls);
};
}

public static Resolution(): ScopeHandler {
return function ResolutionScope(cls, args, _, resolutionContainer) {
return resolutionContainer.getInstance(cls, args);
};
}

public static Scoped = Scopes.Container;
}
1 change: 1 addition & 0 deletions src/token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class Token<T extends new (...args: any[]) => any> {}
46 changes: 46 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { Container } from "./container";
import type { Token } from "./token";

export type ScopeHandler = (
cls: any,
args: any[],
container: Container,
resolutionContainer: Container
) => any;

export interface ScopedClass {
new (...args: any[]): any;

scope: ScopeHandler;
}

export type Injectable<
C extends I,
I extends ScopedClass = ScopedClass
> = InstanceType<I>;

export type TokenDescriptor = {
token?: Token<any>;
class: ScopedClass;
scope?: ScopeHandler;
};

export interface IContainer {
inject<T extends ScopedClass, Args extends any[]>(
cls: T | Token<T>,
...args: Args
): InstanceType<T>;

injectAsync<T extends ScopedClass, Args extends any[]>(
cls: T | Token<T>,
...args: Args
): Promise<InstanceType<T>>;

childContainer(name?: string): Container;

register(tokenDescriptor: TokenDescriptor): void;

unregister(token: Token<any> | ScopedClass): void;

reset(): void;
}
Loading

0 comments on commit bad10c5

Please sign in to comment.