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
+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 })
}
}
+44 -27
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.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()
}
}
+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>
}