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/Injector';
|
||||
export * from './src/Types';
|
||||
export * from './src/Interfaces';
|
||||
|
||||
+1
-1
@@ -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
@@ -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 })
|
||||
}
|
||||
}
|
||||
+44
-27
@@ -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.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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 { Initializable } from "../../src/Interfaces";
|
||||
import { COMPONENT_A_VALUE } from "../CONSTANTS";
|
||||
|
||||
@Singleton()
|
||||
export class ComponentA{
|
||||
export class ComponentA implements Initializable{
|
||||
private value: string
|
||||
|
||||
getFromThis(): string {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 { 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)
|
||||
|
||||
})
|
||||
})
|
||||
@@ -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,2 +1,3 @@
|
||||
export const COMPONENT_A_VALUE = "ComponentA"
|
||||
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