From 963a3a9652412a1d91f9b0c706a3cfe69d094f04 Mon Sep 17 00:00:00 2001 From: nitowa Date: Mon, 25 Jul 2022 10:08:39 +0200 Subject: [PATCH] better code structure in injector/decorator and implemented initialization priorities --- Index.ts | 2 +- package.json | 2 +- src/Decorator.ts | 21 ++++--- src/Injector.ts | 71 +++++++++++++++--------- src/Interfaces.ts | 8 +++ src/Internals.ts | 37 ++++++++++++ src/Strings.ts | 4 ++ src/Types.ts | 17 ------ test/BasicTest/ComponentA.ts | 3 +- test/BasicTest/ComponentB.ts | 7 ++- test/BasicTest/ComponentC.ts | 28 ++++++++++ test/BasicTest/Test.ts | 5 +- test/BasicTest/TestComponent.ts | 8 +++ test/CONSTANTS.ts | 3 +- test/InitializationTest/ComponentA.ts | 16 ++++++ test/InitializationTest/ComponentB.ts | 16 ++++++ test/InitializationTest/ComponentC.ts | 16 ++++++ test/InitializationTest/Test.ts | 19 +++++++ test/InitializationTest/TestComponent.ts | 16 ++++++ 19 files changed, 240 insertions(+), 59 deletions(-) create mode 100644 src/Interfaces.ts create mode 100644 src/Internals.ts create mode 100644 src/Strings.ts delete mode 100644 src/Types.ts create mode 100644 test/BasicTest/ComponentC.ts create mode 100644 test/InitializationTest/ComponentA.ts create mode 100644 test/InitializationTest/ComponentB.ts create mode 100644 test/InitializationTest/ComponentC.ts create mode 100644 test/InitializationTest/Test.ts create mode 100644 test/InitializationTest/TestComponent.ts diff --git a/Index.ts b/Index.ts index 944e780..dd989eb 100644 --- a/Index.ts +++ b/Index.ts @@ -1,3 +1,3 @@ export * from './src/Decorator'; export * from './src/Injector'; -export * from './src/Types'; +export * from './src/Interfaces'; diff --git a/package.json b/package.json index 640a277..fedca74 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "tsc": "tsc", "build": "npm run clean && tsc", "clean": "rm -rf js", - "test": "npm run clean && npm run build && mocha --recursive --bail=true js/test" + "test": "npm run clean && npm run build && mocha --bail=true js/test/BasicTest && mocha --bail=true js/test/InitializationTest" }, "license": "MIT", "dependencies": { diff --git a/src/Decorator.ts b/src/Decorator.ts index 8e65a01..99f5de3 100644 --- a/src/Decorator.ts +++ b/src/Decorator.ts @@ -1,21 +1,28 @@ import { Injector } from "./Injector"; -import { Type, GenericClassDecorator, Constructor } from "./Types"; +import { Type, GenericClassDecorator, Constructor } from "./Internals"; /** * @returns {GenericClassDecorator>} * @constructor */ -export function Singleton(_interface?: Constructor): GenericClassDecorator> { +export function Singleton(config?: { + interface?: Constructor, + initializationPriority?: number +}): GenericClassDecorator> { return (clazz: Type) => { - Injector['modules'].push({ - implements: _interface ?? clazz, + Injector['singletonDefinitions'].push({ + initializationPriority: config ?. initializationPriority, ctor: clazz }) + + if(config && config.interface){ + Injector['tokenLookupTable'][config.interface.name] = clazz + } } } -export function Inject(clazz: Constructor) { - return function (instance: Object, key: string) { - Injector['injectionQueue'].push({ injectionType: clazz, instance: instance, injectIntoKey: key }) +export function Inject(token: Constructor) { + return function (receiver: Object, key: string) { + Injector['injectionQueue'].push({ token, receiver, key }) } } \ No newline at end of file diff --git a/src/Injector.ts b/src/Injector.ts index 7a609d7..18a3657 100644 --- a/src/Injector.ts +++ b/src/Injector.ts @@ -1,69 +1,86 @@ import 'reflect-metadata'; -import { Constructor, Type } from './Types'; +import { ERR_NO_INITIALIZE_WITH_PRIORITY, ERR_NO_INJECTION_TOKEN } from './Strings'; +import { Constructor, Type, Module as SingletonDefinition, InjectionError, InjectionResolutionError } from './Internals'; class _Injector { private injectionQueue: any[] = [] - private modules: { implements?: Constructor, ctor: Type }[] = [] - private moduleObjs: { [key in string]: any } = {} + private singletonDefinitions: SingletonDefinition[] = [] + + private singletonObjects: { [classname in string]: any } = {} + private tokenLookupTable: { [token in string]: Constructor } = {} + private initialized = false /** * Resolves instances by injecting required services - * @param {Type} target + * @param {Type} request * @returns {T} */ - public resolve(target: Constructor): T { + public resolve(request: Constructor): T { if (!this.initialized) { this.initialize() this.initialized = true } - return this.moduleObjs[target.name] as any + return this.singletonObjects[request.name] as any } public async resolveAsync(target: Type): Promise { if (!this.initialized) { - await this.initialize() + this.initialize() this.initialized = true } - return this.moduleObjs[target.name] as any + return this.singletonObjects[target.name] as any } - private initialize = async (async?: boolean) => { + private initialize = () => { this.createSingletons() this.injectDependencies() - if (async) - await this.initializeSingletons() - else - this.initializeSingletons() + this.initializeSingletons() + this.cleanup() } private createSingletons = () => { - //instantiate all non-root modules - this.modules.forEach(m => { - const module = new m.ctor() - if (m.implements) - this.moduleObjs[m.implements.name] = module - this.moduleObjs[m.ctor.name] = module + this.singletonDefinitions.forEach(def => { + const obj = new def.ctor() + + if (def.initializationPriority != undefined && !obj.initialize) { + throw new InjectionError(ERR_NO_INITIALIZE_WITH_PRIORITY(def.ctor)) + } + + this.singletonObjects[def.ctor.name] = obj }) } private injectDependencies = () => { - while (this.injectionQueue.length > 0) { - const inj = this.injectionQueue.shift() + this.injectionQueue.forEach(inj => { - if (this.moduleObjs[inj.injectionType.name]) { - this.moduleObjs[inj.instance.constructor.name][inj.injectIntoKey] = this.moduleObjs[inj.injectionType.name] - } else { - throw new Error("Cannot resolve injection token " + inj.injectionType.name) + if (inj.token.name in this.tokenLookupTable) { //injection alias was used + inj.token = this.tokenLookupTable[inj.token.name] } - } + + if (this.singletonObjects[inj.token.name]) { + this.singletonObjects[inj.receiver.constructor.name][inj.key] = this.singletonObjects[inj.token.name] + } else { + throw new InjectionResolutionError(ERR_NO_INJECTION_TOKEN(inj.injectionType)) + } + }) } private initializeSingletons = () => { - Object.values(this.moduleObjs).forEach(element => element.initialize ? element.initialize() : undefined); + this.singletonDefinitions + .sort((a, b) => (a.initializationPriority ?? 0) - (b.initializationPriority ?? 0)) + .map(def => this.singletonObjects[def.ctor.name]) + .forEach(obj => obj.initialize ? obj.initialize() : undefined) + } + + private cleanup = () => { + while (this.singletonDefinitions.length > 0) + this.singletonDefinitions.pop() + while (this.injectionQueue.length > 0) + this.injectionQueue.pop() } } diff --git a/src/Interfaces.ts b/src/Interfaces.ts new file mode 100644 index 0000000..b54729f --- /dev/null +++ b/src/Interfaces.ts @@ -0,0 +1,8 @@ + +export interface Initializable { + initialize: () => void +} + +export interface AsyncInitializable { + initialize: () => void | Promise +} \ No newline at end of file diff --git a/src/Internals.ts b/src/Internals.ts new file mode 100644 index 0000000..61e73c4 --- /dev/null +++ b/src/Internals.ts @@ -0,0 +1,37 @@ +/** + * Type for what object is instances of. Also applicable to "Constructor of T" as Types/Classes/Constructors are interchangable in TS. + */ +export interface Type { + new(...args: any[]): T; +} + +export type Constructor = Function & { prototype: T } + +/** + * Generic `ClassDecorator` type + */ +export type GenericClassDecorator = (target: T) => void; + +export type Module = { + initializationPriority?: number //Priority of initializing this object after creation + ctor: Type //Object constructor to make singleton from +} + +export class NamedError extends Error{ + constructor(message: string){ + super(message) + this.name = this.constructor.name + } +} + +export class InjectionError extends NamedError{ + constructor(message: string){ + super(message) + } +} + +export class InjectionResolutionError extends InjectionError{ + constructor(message: string){ + super(message) + } +} \ No newline at end of file diff --git a/src/Strings.ts b/src/Strings.ts new file mode 100644 index 0000000..34b49c9 --- /dev/null +++ b/src/Strings.ts @@ -0,0 +1,4 @@ +import { Constructor } from "./Internals"; + +export const ERR_NO_INITIALIZE_WITH_PRIORITY = (ctor: Constructor) => `The singleton class '${ctor.name}' specified an initialization priority but has no initialize() function. Either remove the 'initializationPriority' parameter or add a function of the signature 'public initialize():void'.` +export const ERR_NO_INJECTION_TOKEN = (ctor: Constructor) => `Could not resolve a singleton for '${ctor.name}'. Make sure the class is marked as '@Injectable()'. If a resolution token other than the classname is requested make sure it is registered via the 'interface' parameter.` \ No newline at end of file diff --git a/src/Types.ts b/src/Types.ts deleted file mode 100644 index 3756f03..0000000 --- a/src/Types.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Type for what object is instances of. Also applicable to "Constructor of T" as Types/Classes/Constructors are interchangable in TS. - */ -export interface Type { - new(...args: any[]): T; -} - -export type Constructor = Function & { prototype: T } - -/** - * Generic `ClassDecorator` type - */ -export type GenericClassDecorator = (target: T) => void; - -export interface ISingleton{ - initialize?(): void | Promise -} \ No newline at end of file diff --git a/test/BasicTest/ComponentA.ts b/test/BasicTest/ComponentA.ts index 7d09124..a0f21cb 100644 --- a/test/BasicTest/ComponentA.ts +++ b/test/BasicTest/ComponentA.ts @@ -1,8 +1,9 @@ import { Singleton } from "../../src/Decorator"; +import { Initializable } from "../../src/Interfaces"; import { COMPONENT_A_VALUE } from "../CONSTANTS"; @Singleton() -export class ComponentA{ +export class ComponentA implements Initializable{ private value: string getFromThis(): string { diff --git a/test/BasicTest/ComponentB.ts b/test/BasicTest/ComponentB.ts index 633a1d2..3d4c427 100644 --- a/test/BasicTest/ComponentB.ts +++ b/test/BasicTest/ComponentB.ts @@ -1,4 +1,5 @@ import { Inject, Singleton } from "../../src/Decorator" +import { Initializable } from "../../src/Interfaces" import { COMPONENT_B_VALUE } from "../CONSTANTS" import { ComponentA } from "./ComponentA" @@ -7,8 +8,10 @@ export abstract class IComponentB{ getFromThis: () => string } -@Singleton(IComponentB) -export class ComponentB implements IComponentB{ +@Singleton({ + interface: IComponentB +}) +export class ComponentB implements IComponentB, Initializable{ @Inject(ComponentA) private componentA: ComponentA diff --git a/test/BasicTest/ComponentC.ts b/test/BasicTest/ComponentC.ts new file mode 100644 index 0000000..d8868e5 --- /dev/null +++ b/test/BasicTest/ComponentC.ts @@ -0,0 +1,28 @@ +import { Inject, Singleton } from "../../src/Decorator" +import { Initializable } from "../../src/Interfaces" +import { COMPONENT_B_VALUE, COMPONENT_C_VALUE } from "../CONSTANTS" +import { ComponentA } from "./ComponentA" + +export abstract class IComponentC{ + getFromA: () => string + getFromThis: () => string +} + +@Singleton({ + interface: IComponentC, +}) +export class ComponentC implements IComponentC{ + + @Inject(ComponentA) + private componentA: ComponentA + + private value: string = COMPONENT_C_VALUE + + getFromA(): string { + return this.componentA.getFromThis() + } + + getFromThis(): string { + return this.value + } +} \ No newline at end of file diff --git a/test/BasicTest/Test.ts b/test/BasicTest/Test.ts index cbd8891..6316685 100644 --- a/test/BasicTest/Test.ts +++ b/test/BasicTest/Test.ts @@ -1,7 +1,7 @@ import { Injector } from '../../src/Injector' import { TestComponent } from './TestComponent' -import { assert, expect } from 'chai'; -import { COMPONENT_A_VALUE, COMPONENT_B_VALUE } from '../CONSTANTS'; +import { expect } from 'chai'; +import { COMPONENT_A_VALUE, COMPONENT_B_VALUE, COMPONENT_C_VALUE } from '../CONSTANTS'; var should = require('chai').should(); var chai = require("chai"); @@ -17,6 +17,7 @@ describe('dependjs', () => { expect(testComp.getFromA()).to.be.equal(COMPONENT_A_VALUE) expect(testComp.getAThroughB()).to.be.equal(COMPONENT_A_VALUE) expect(testComp.getFromB()).to.be.equal(COMPONENT_B_VALUE) + expect(testComp.getFromC()).to.be.equal(COMPONENT_C_VALUE) }) }) \ No newline at end of file diff --git a/test/BasicTest/TestComponent.ts b/test/BasicTest/TestComponent.ts index a84d5a1..404aeac 100644 --- a/test/BasicTest/TestComponent.ts +++ b/test/BasicTest/TestComponent.ts @@ -1,6 +1,8 @@ import { Inject, Singleton } from "../../src/Decorator"; +import { Initializable } from "../../src/Interfaces"; import {ComponentA} from "./ComponentA" import {IComponentB} from "./ComponentB" +import { ComponentC } from "./ComponentC"; @Singleton() export class TestComponent{ @@ -11,6 +13,9 @@ export class TestComponent{ @Inject(ComponentA) private componentA: ComponentA + @Inject(ComponentC) + private componentC: ComponentC + getFromA(): string{ return this.componentA.getFromThis() } @@ -23,4 +28,7 @@ export class TestComponent{ return this.compoenntB.getFromThis() } + getFromC(): string{ + return this.componentC.getFromThis() + } } \ No newline at end of file diff --git a/test/CONSTANTS.ts b/test/CONSTANTS.ts index ec64f5b..08e199f 100644 --- a/test/CONSTANTS.ts +++ b/test/CONSTANTS.ts @@ -1,2 +1,3 @@ export const COMPONENT_A_VALUE = "ComponentA" -export const COMPONENT_B_VALUE = "ComponentB" \ No newline at end of file +export const COMPONENT_B_VALUE = "ComponentB" +export const COMPONENT_C_VALUE = "ComponentC" \ No newline at end of file diff --git a/test/InitializationTest/ComponentA.ts b/test/InitializationTest/ComponentA.ts new file mode 100644 index 0000000..4cdc788 --- /dev/null +++ b/test/InitializationTest/ComponentA.ts @@ -0,0 +1,16 @@ +import { Inject, Singleton } from "../../src/Decorator" +import { Initializable } from "../../src/Interfaces" +import { COMPONENT_A_VALUE } from "../CONSTANTS" +import { TestComponent } from "./TestComponent" + +@Singleton({ + initializationPriority: 3 +}) +export class ComponentA implements Initializable{ + @Inject(TestComponent) + private testComponent: TestComponent + + initialize(): void{ + this.testComponent.pushData(COMPONENT_A_VALUE) + } +} \ No newline at end of file diff --git a/test/InitializationTest/ComponentB.ts b/test/InitializationTest/ComponentB.ts new file mode 100644 index 0000000..f13048a --- /dev/null +++ b/test/InitializationTest/ComponentB.ts @@ -0,0 +1,16 @@ +import { Inject, Singleton } from "../../src/Decorator" +import { Initializable } from "../../src/Interfaces" +import { COMPONENT_B_VALUE } from "../CONSTANTS" +import { TestComponent } from "./TestComponent" + +@Singleton({ + initializationPriority: 2 +}) +export class ComponentB implements Initializable{ + @Inject(TestComponent) + private testComponent: TestComponent + + initialize(): void{ + this.testComponent.pushData(COMPONENT_B_VALUE) + } +} \ No newline at end of file diff --git a/test/InitializationTest/ComponentC.ts b/test/InitializationTest/ComponentC.ts new file mode 100644 index 0000000..46803c6 --- /dev/null +++ b/test/InitializationTest/ComponentC.ts @@ -0,0 +1,16 @@ +import { Inject, Singleton } from "../../src/Decorator" +import { Initializable } from "../../src/Interfaces" +import { COMPONENT_C_VALUE } from "../CONSTANTS" +import { TestComponent } from "./TestComponent" + +@Singleton({ + initializationPriority: 1 +}) +export class ComponentC implements Initializable{ + @Inject(TestComponent) + private testComponent: TestComponent + + initialize(): void{ + this.testComponent.pushData(COMPONENT_C_VALUE) + } +} \ No newline at end of file diff --git a/test/InitializationTest/Test.ts b/test/InitializationTest/Test.ts new file mode 100644 index 0000000..57da839 --- /dev/null +++ b/test/InitializationTest/Test.ts @@ -0,0 +1,19 @@ +import { expect } from 'chai'; +import { Injector } from '../../src/Injector' +import { COMPONENT_A_VALUE, COMPONENT_B_VALUE, COMPONENT_C_VALUE } from '../CONSTANTS'; +import { TestComponent } from './TestComponent' + +var chai = require("chai"); +var chaiAsPromised = require("chai-as-promised"); + +chai.use(chaiAsPromised); + +describe('dependjs', () => { + it('initialized in the requested order', () => { + + const testComp = Injector.resolve(TestComponent) + const data = testComp.getData() + + expect(data).to.eql([COMPONENT_C_VALUE, COMPONENT_B_VALUE, COMPONENT_A_VALUE]) + }) +}) \ No newline at end of file diff --git a/test/InitializationTest/TestComponent.ts b/test/InitializationTest/TestComponent.ts new file mode 100644 index 0000000..9614b52 --- /dev/null +++ b/test/InitializationTest/TestComponent.ts @@ -0,0 +1,16 @@ +import { Singleton } from "../../src/Decorator"; + +@Singleton() +export class TestComponent{ + + private data: string[] = [] + + pushData(str: string){ + this.data.push(str) + } + + getData(){ + return this.data + } + +} \ No newline at end of file