Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions packages/devextreme/js/__internal/core/di/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/* eslint-disable @typescript-eslint/no-extraneous-class */
/* eslint-disable prefer-const */
/* eslint-disable @typescript-eslint/init-declarations */
/* eslint-disable max-classes-per-file */
/* eslint-disable class-methods-use-this */
import { describe, expect, it } from '@jest/globals';

import { DIContext } from './index';

describe('basic', () => {
describe('register', () => {
class MyClass {
static dependencies = [] as const;

getNumber(): number {
return 1;
}
}

it('should return registered class', () => {
const ctx = new DIContext();
ctx.register(MyClass);

expect(ctx.get(MyClass)).toBeInstanceOf(MyClass);
expect(ctx.get(MyClass).getNumber()).toBe(1);
});

it('should return registered class with tryGet', () => {
const ctx = new DIContext();
ctx.register(MyClass);

expect(ctx.tryGet(MyClass)).toBeInstanceOf(MyClass);
expect(ctx.tryGet(MyClass)?.getNumber()).toBe(1);
});

it('should return same instance each time', () => {
const ctx = new DIContext();
ctx.register(MyClass);

expect(ctx.get(MyClass)).toBe(ctx.get(MyClass));
});
});

describe('registerInstance', () => {
class MyClass {
static dependencies = [] as const;

getNumber(): number {
return 1;
}
}

const ctx = new DIContext();
const instance = new MyClass();
ctx.registerInstance(MyClass, instance);

it('should work', () => {
expect(ctx.get(MyClass)).toBe(instance);
});
});

describe('non registered items', () => {
const ctx = new DIContext();
class MyClass {
static dependencies = [] as const;

getNumber(): number {
return 1;
}
}
it('should throw', () => {
expect(() => ctx.get(MyClass)).toThrow();
});
it('should not throw if tryGet', () => {
expect(ctx.tryGet(MyClass)).toBe(null);
});
});
});

describe('dependencies', () => {
class MyUtilityClass {
static dependencies = [] as const;

getNumber(): number {
return 2;
}
}

class MyClass {
static dependencies = [MyUtilityClass] as const;

constructor(private readonly utility: MyUtilityClass) {}

getSuperNumber(): number {
return this.utility.getNumber() * 2;
}
}

const ctx = new DIContext();
ctx.register(MyUtilityClass);
ctx.register(MyClass);

it('should return registered class', () => {
expect(ctx.get(MyClass)).toBeInstanceOf(MyClass);
expect(ctx.get(MyUtilityClass)).toBeInstanceOf(MyUtilityClass);
});

it('dependecies should work', () => {
expect(ctx.get(MyClass).getSuperNumber()).toBe(4);
});
});

describe('mocks', () => {
class MyClass {
static dependencies = [] as const;

getNumber(): number {
return 1;
}
}

class MyClassMock implements MyClass {
static dependencies = [] as const;

getNumber(): number {
return 2;
}
}

const ctx = new DIContext();
ctx.register(MyClass, MyClassMock);

it('should return mock class when they are registered', () => {
expect(ctx.get(MyClass)).toBeInstanceOf(MyClassMock);
expect(ctx.get(MyClass).getNumber()).toBe(2);
});
});

it('should work regardless of registration order', () => {
class MyClass {
static dependencies = [] as const;

getNumber(): number {
return 1;
}
}

class MyDependentClass {
static dependencies = [MyClass] as const;

constructor(private readonly myClass: MyClass) {}

getSuperNumber(): number {
return this.myClass.getNumber() * 2;
}
}

const ctx = new DIContext();
ctx.register(MyDependentClass);
ctx.register(MyClass);
expect(ctx.get(MyDependentClass).getSuperNumber()).toBe(2);
});

describe('dependency cycle', () => {
class MyClass1 {
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/no-use-before-define
static dependencies = [MyClass2] as const;

constructor(private readonly myClass2: MyClass2) {}
}
class MyClass2 {
static dependencies = [MyClass1] as const;

constructor(private readonly myClass1: MyClass1) {}
}

const ctx = new DIContext();
ctx.register(MyClass1);
ctx.register(MyClass2);

it('should throw', () => {
expect(() => ctx.get(MyClass1)).toThrow();
expect(() => ctx.get(MyClass2)).toThrow();
});
});
90 changes: 90 additions & 0 deletions packages/devextreme/js/__internal/core/di/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/* eslint-disable spellcheck/spell-checker */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-explicit-any */

// eslint-disable-next-line @typescript-eslint/ban-types
interface AbstractType<T> extends Function {
prototype: T;
}

type Constructor<T, TDeps extends readonly any[]> = new(...deps: TDeps) => T;

interface DIItem<T, TDeps extends readonly any[]> extends Constructor<T, TDeps> {
dependencies: readonly [...{ [P in keyof TDeps]: AbstractType<TDeps[P]> }];
}

export class DIContext {
private readonly instances: Map<unknown, unknown> = new Map();

private readonly fabrics: Map<unknown, unknown> = new Map();

private readonly antiRecursionSet = new Set();

public register<TId, TFabric extends TId, TDeps extends readonly any[]>(
id: AbstractType<TId>,
fabric: DIItem<TFabric, TDeps>,
): void;
public register<T, TDeps extends readonly any[]>(
idAndFabric: DIItem<T, TDeps>,
): void;
public register<T, TDeps extends readonly any[]>(
id: DIItem<T, TDeps>,
fabric?: DIItem<T, TDeps>,
): void {
// eslint-disable-next-line no-param-reassign
fabric ??= id;
this.fabrics.set(id, fabric);
}

public registerInstance<T>(
id: AbstractType<T>,
instance: T,
): void {
this.instances.set(id, instance);
}

public get<T>(
id: AbstractType<T>,
): T {
const instance = this.tryGet(id);

if (instance) {
return instance;
}

throw new Error('DI item is not registered');
}

public tryGet<T>(
id: AbstractType<T>,
): T | null {
if (this.instances.get(id)) {
return this.instances.get(id) as T;
}

const fabric = this.fabrics.get(id);
if (fabric) {
const res: T = this.create(fabric as any);
this.instances.set(id, res);
this.instances.set(fabric, res);
return res;
}

return null;
}

private create<T, TDeps extends readonly any[]>(fabric: DIItem<T, TDeps>): T {
if (this.antiRecursionSet.has(fabric)) {
throw new Error('dependency cycle in DI');
}

this.antiRecursionSet.add(fabric);

const args = fabric.dependencies.map((dependency) => this.get(dependency));

this.antiRecursionSet.delete(fabric);

// eslint-disable-next-line new-cap
return new fabric(...args as any);
}
}
Loading