better code structure in injector/decorator and implemented initialization priorities
This commit is contained in:
@@ -1,3 +1,3 @@
|
|||||||
export * from './src/Decorator';
|
export * from './src/Decorator';
|
||||||
export * from './src/Injector';
|
export * from './src/Injector';
|
||||||
export * from './src/Types';
|
export * from './src/Interfaces';
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@
|
|||||||
"tsc": "tsc",
|
"tsc": "tsc",
|
||||||
"build": "npm run clean && tsc",
|
"build": "npm run clean && tsc",
|
||||||
"clean": "rm -rf js",
|
"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",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+14
-7
@@ -1,21 +1,28 @@
|
|||||||
import { Injector } from "./Injector";
|
import { Injector } from "./Injector";
|
||||||
import { Type, GenericClassDecorator, Constructor } from "./Types";
|
import { Type, GenericClassDecorator, Constructor } from "./Internals";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {GenericClassDecorator<Type<any>>}
|
* @returns {GenericClassDecorator<Type<any>>}
|
||||||
* @constructor
|
* @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>) => {
|
return (clazz: Type<any>) => {
|
||||||
Injector['modules'].push({
|
Injector['singletonDefinitions'].push({
|
||||||
implements: _interface ?? clazz,
|
initializationPriority: config ?. initializationPriority,
|
||||||
ctor: clazz
|
ctor: clazz
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if(config && config.interface){
|
||||||
|
Injector['tokenLookupTable'][config.interface.name] = clazz
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Inject(clazz: Constructor<any>) {
|
export function Inject(token: Constructor<any>) {
|
||||||
return function (instance: Object, key: string) {
|
return function (receiver: Object, key: string) {
|
||||||
Injector['injectionQueue'].push({ injectionType: clazz, instance: instance, injectIntoKey: key })
|
Injector['injectionQueue'].push({ token, receiver, key })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+44
-27
@@ -1,69 +1,86 @@
|
|||||||
import 'reflect-metadata';
|
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 {
|
class _Injector {
|
||||||
|
|
||||||
private injectionQueue: any[] = []
|
private injectionQueue: any[] = []
|
||||||
private modules: { implements?: Constructor<any>, ctor: Type<any> }[] = []
|
private singletonDefinitions: SingletonDefinition[] = []
|
||||||
private moduleObjs: { [key in string]: any } = {}
|
|
||||||
|
private singletonObjects: { [classname in string]: any } = {}
|
||||||
|
private tokenLookupTable: { [token in string]: Constructor<any> } = {}
|
||||||
|
|
||||||
private initialized = false
|
private initialized = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves instances by injecting required services
|
* Resolves instances by injecting required services
|
||||||
* @param {Type<any>} target
|
* @param {Type<any>} request
|
||||||
* @returns {T}
|
* @returns {T}
|
||||||
*/
|
*/
|
||||||
public resolve<T>(target: Constructor<T>): T {
|
public resolve<T>(request: Constructor<T>): T {
|
||||||
if (!this.initialized) {
|
if (!this.initialized) {
|
||||||
this.initialize()
|
this.initialize()
|
||||||
this.initialized = true
|
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> {
|
public async resolveAsync<T>(target: Type<T>): Promise<T> {
|
||||||
if (!this.initialized) {
|
if (!this.initialized) {
|
||||||
await this.initialize()
|
this.initialize()
|
||||||
this.initialized = true
|
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.createSingletons()
|
||||||
this.injectDependencies()
|
this.injectDependencies()
|
||||||
if (async)
|
this.initializeSingletons()
|
||||||
await this.initializeSingletons()
|
this.cleanup()
|
||||||
else
|
|
||||||
this.initializeSingletons()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private createSingletons = () => {
|
private createSingletons = () => {
|
||||||
//instantiate all non-root modules
|
this.singletonDefinitions.forEach(def => {
|
||||||
this.modules.forEach(m => {
|
const obj = new def.ctor()
|
||||||
const module = new m.ctor()
|
|
||||||
if (m.implements)
|
if (def.initializationPriority != undefined && !obj.initialize) {
|
||||||
this.moduleObjs[m.implements.name] = module
|
throw new InjectionError(ERR_NO_INITIALIZE_WITH_PRIORITY(def.ctor))
|
||||||
this.moduleObjs[m.ctor.name] = module
|
}
|
||||||
|
|
||||||
|
this.singletonObjects[def.ctor.name] = obj
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private injectDependencies = () => {
|
private injectDependencies = () => {
|
||||||
while (this.injectionQueue.length > 0) {
|
this.injectionQueue.forEach(inj => {
|
||||||
const inj = this.injectionQueue.shift()
|
|
||||||
|
|
||||||
if (this.moduleObjs[inj.injectionType.name]) {
|
if (inj.token.name in this.tokenLookupTable) { //injection alias was used
|
||||||
this.moduleObjs[inj.instance.constructor.name][inj.injectIntoKey] = this.moduleObjs[inj.injectionType.name]
|
inj.token = this.tokenLookupTable[inj.token.name]
|
||||||
} else {
|
|
||||||
throw new Error("Cannot resolve injection token " + inj.injectionType.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 = () => {
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
export interface Initializable {
|
||||||
|
initialize: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsyncInitializable {
|
||||||
|
initialize: () => void | Promise<void>
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.`
|
||||||
@@ -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>
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Singleton } from "../../src/Decorator";
|
import { Singleton } from "../../src/Decorator";
|
||||||
|
import { Initializable } from "../../src/Interfaces";
|
||||||
import { COMPONENT_A_VALUE } from "../CONSTANTS";
|
import { COMPONENT_A_VALUE } from "../CONSTANTS";
|
||||||
|
|
||||||
@Singleton()
|
@Singleton()
|
||||||
export class ComponentA{
|
export class ComponentA implements Initializable{
|
||||||
private value: string
|
private value: string
|
||||||
|
|
||||||
getFromThis(): string {
|
getFromThis(): string {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Inject, Singleton } from "../../src/Decorator"
|
import { Inject, Singleton } from "../../src/Decorator"
|
||||||
|
import { Initializable } from "../../src/Interfaces"
|
||||||
import { COMPONENT_B_VALUE } from "../CONSTANTS"
|
import { COMPONENT_B_VALUE } from "../CONSTANTS"
|
||||||
import { ComponentA } from "./ComponentA"
|
import { ComponentA } from "./ComponentA"
|
||||||
|
|
||||||
@@ -7,8 +8,10 @@ export abstract class IComponentB{
|
|||||||
getFromThis: () => string
|
getFromThis: () => string
|
||||||
}
|
}
|
||||||
|
|
||||||
@Singleton(IComponentB)
|
@Singleton({
|
||||||
export class ComponentB implements IComponentB{
|
interface: IComponentB
|
||||||
|
})
|
||||||
|
export class ComponentB implements IComponentB, Initializable{
|
||||||
|
|
||||||
@Inject(ComponentA)
|
@Inject(ComponentA)
|
||||||
private componentA: ComponentA
|
private componentA: ComponentA
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Injector } from '../../src/Injector'
|
import { Injector } from '../../src/Injector'
|
||||||
import { TestComponent } from './TestComponent'
|
import { TestComponent } from './TestComponent'
|
||||||
import { assert, expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { COMPONENT_A_VALUE, COMPONENT_B_VALUE } from '../CONSTANTS';
|
import { COMPONENT_A_VALUE, COMPONENT_B_VALUE, COMPONENT_C_VALUE } from '../CONSTANTS';
|
||||||
|
|
||||||
var should = require('chai').should();
|
var should = require('chai').should();
|
||||||
var chai = require("chai");
|
var chai = require("chai");
|
||||||
@@ -17,6 +17,7 @@ describe('dependjs', () => {
|
|||||||
expect(testComp.getFromA()).to.be.equal(COMPONENT_A_VALUE)
|
expect(testComp.getFromA()).to.be.equal(COMPONENT_A_VALUE)
|
||||||
expect(testComp.getAThroughB()).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.getFromB()).to.be.equal(COMPONENT_B_VALUE)
|
||||||
|
expect(testComp.getFromC()).to.be.equal(COMPONENT_C_VALUE)
|
||||||
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Inject, Singleton } from "../../src/Decorator";
|
import { Inject, Singleton } from "../../src/Decorator";
|
||||||
|
import { Initializable } from "../../src/Interfaces";
|
||||||
import {ComponentA} from "./ComponentA"
|
import {ComponentA} from "./ComponentA"
|
||||||
import {IComponentB} from "./ComponentB"
|
import {IComponentB} from "./ComponentB"
|
||||||
|
import { ComponentC } from "./ComponentC";
|
||||||
|
|
||||||
@Singleton()
|
@Singleton()
|
||||||
export class TestComponent{
|
export class TestComponent{
|
||||||
@@ -11,6 +13,9 @@ export class TestComponent{
|
|||||||
@Inject(ComponentA)
|
@Inject(ComponentA)
|
||||||
private componentA: ComponentA
|
private componentA: ComponentA
|
||||||
|
|
||||||
|
@Inject(ComponentC)
|
||||||
|
private componentC: ComponentC
|
||||||
|
|
||||||
getFromA(): string{
|
getFromA(): string{
|
||||||
return this.componentA.getFromThis()
|
return this.componentA.getFromThis()
|
||||||
}
|
}
|
||||||
@@ -23,4 +28,7 @@ export class TestComponent{
|
|||||||
return this.compoenntB.getFromThis()
|
return this.compoenntB.getFromThis()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFromC(): string{
|
||||||
|
return this.componentC.getFromThis()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export const COMPONENT_A_VALUE = "ComponentA"
|
export const COMPONENT_A_VALUE = "ComponentA"
|
||||||
export const COMPONENT_B_VALUE = "ComponentB"
|
export const COMPONENT_B_VALUE = "ComponentB"
|
||||||
|
export const COMPONENT_C_VALUE = "ComponentC"
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user