better code structure in injector/decorator and implemented initialization priorities

This commit is contained in:
nitowa
2022-07-25 10:08:39 +02:00
parent 076064ad98
commit 963a3a9652
19 changed files with 240 additions and 59 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
export * from './src/Decorator';
export * from './src/Injector';
export * from './src/Types';
export * from './src/Interfaces';
+1 -1
View File
@@ -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": {
+14 -7
View File
@@ -1,21 +1,28 @@
import { Injector } from "./Injector";
import { Type, GenericClassDecorator, Constructor } from "./Types";
import { Type, GenericClassDecorator, Constructor } from "./Internals";
/**
* @returns {GenericClassDecorator<Type<any>>}
* @constructor
*/
export function Singleton(_interface?: Constructor<any>): GenericClassDecorator<Type<any>> {
export function Singleton(config?: {
interface?: Constructor<any>,
initializationPriority?: number
}): GenericClassDecorator<Type<any>> {
return (clazz: Type<any>) => {
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<any>) {
return function (instance: Object, key: string) {
Injector['injectionQueue'].push({ injectionType: clazz, instance: instance, injectIntoKey: key })
export function Inject(token: Constructor<any>) {
return function (receiver: Object, key: string) {
Injector['injectionQueue'].push({ token, receiver, key })
}
}
+42 -25
View File
@@ -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<any>, ctor: Type<any> }[] = []
private moduleObjs: { [key in string]: any } = {}
private singletonDefinitions: SingletonDefinition[] = []
private singletonObjects: { [classname in string]: any } = {}
private tokenLookupTable: { [token in string]: Constructor<any> } = {}
private initialized = false
/**
* Resolves instances by injecting required services
* @param {Type<any>} target
* @param {Type<any>} request
* @returns {T}
*/
public resolve<T>(target: Constructor<T>): T {
public resolve<T>(request: Constructor<T>): 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<T>(target: Type<T>): Promise<T> {
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.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]
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 Error("Cannot resolve injection token " + inj.injectionType.name)
}
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()
}
}
+8
View File
@@ -0,0 +1,8 @@
export interface Initializable {
initialize: () => void
}
export interface AsyncInitializable {
initialize: () => void | Promise<void>
}
+37
View File
@@ -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<T> {
new(...args: any[]): T;
}
export type Constructor<T> = Function & { prototype: T }
/**
* Generic `ClassDecorator` type
*/
export type GenericClassDecorator<T> = (target: T) => void;
export type Module = {
initializationPriority?: number //Priority of initializing this object after creation
ctor: Type<any> //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)
}
}
+4
View File
@@ -0,0 +1,4 @@
import { Constructor } from "./Internals";
export const ERR_NO_INITIALIZE_WITH_PRIORITY = (ctor: Constructor<any>) => `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<any>) => `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.`
-17
View File
@@ -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<T> {
new(...args: any[]): T;
}
export type Constructor<T> = Function & { prototype: T }
/**
* Generic `ClassDecorator` type
*/
export type GenericClassDecorator<T> = (target: T) => void;
export interface ISingleton{
initialize?(): void | Promise<void>
}
+2 -1
View File
@@ -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 {
+5 -2
View File
@@ -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
+28
View File
@@ -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
}
}
+3 -2
View File
@@ -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)
})
})
+8
View File
@@ -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()
}
}
+1
View File
@@ -1,2 +1,3 @@
export const COMPONENT_A_VALUE = "ComponentA"
export const COMPONENT_B_VALUE = "ComponentB"
export const COMPONENT_C_VALUE = "ComponentC"
+16
View File
@@ -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)
}
}
+16
View File
@@ -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)
}
}
+16
View File
@@ -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)
}
}
+19
View File
@@ -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])
})
})
+16
View File
@@ -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
}
}