Browse Source

better code structure in injector/decorator and implemented initialization priorities

master
nitowa 1 year ago
parent
commit
963a3a9652

+ 1
- 1
Index.ts View File

@@ -1,3 +1,3 @@
1 1
 export * from './src/Decorator';
2 2
 export * from './src/Injector';
3
-export * from './src/Types';
3
+export * from './src/Interfaces';

+ 1
- 1
package.json View File

@@ -21,7 +21,7 @@
21 21
     "tsc": "tsc",
22 22
     "build": "npm run clean && tsc",
23 23
     "clean": "rm -rf js",
24
-    "test": "npm run clean && npm run build && mocha --recursive --bail=true js/test"
24
+    "test": "npm run clean && npm run build && mocha --bail=true js/test/BasicTest && mocha --bail=true js/test/InitializationTest"
25 25
   },
26 26
   "license": "MIT",
27 27
   "dependencies": {

+ 14
- 7
src/Decorator.ts View File

@@ -1,21 +1,28 @@
1 1
 import { Injector } from "./Injector";
2
-import { Type, GenericClassDecorator, Constructor } from "./Types";
2
+import { Type, GenericClassDecorator, Constructor } from "./Internals";
3 3
 
4 4
 /**
5 5
  * @returns {GenericClassDecorator<Type<any>>}
6 6
  * @constructor
7 7
  */
8
-export function Singleton(_interface?: Constructor<any>): GenericClassDecorator<Type<any>> {
8
+export function Singleton(config?: {
9
+  interface?: Constructor<any>,
10
+  initializationPriority?: number
11
+}): GenericClassDecorator<Type<any>> {
9 12
   return (clazz: Type<any>) => {
10
-    Injector['modules'].push({
11
-      implements: _interface ?? clazz,
13
+    Injector['singletonDefinitions'].push({
14
+      initializationPriority: config ?. initializationPriority,
12 15
       ctor: clazz
13 16
     })
17
+
18
+    if(config && config.interface){
19
+      Injector['tokenLookupTable'][config.interface.name] = clazz
20
+    }
14 21
   }
15 22
 }
16 23
 
17
-export function Inject(clazz: Constructor<any>) {
18
-  return function (instance: Object, key: string) {
19
-    Injector['injectionQueue'].push({ injectionType: clazz, instance: instance, injectIntoKey: key })
24
+export function Inject(token: Constructor<any>) {
25
+  return function (receiver: Object, key: string) {
26
+    Injector['injectionQueue'].push({ token, receiver, key })
20 27
   }
21 28
 }

+ 43
- 26
src/Injector.ts View File

@@ -1,69 +1,86 @@
1 1
 import 'reflect-metadata';
2
-import { Constructor, Type } from './Types';
2
+import { ERR_NO_INITIALIZE_WITH_PRIORITY, ERR_NO_INJECTION_TOKEN } from './Strings';
3
+import { Constructor, Type, Module as SingletonDefinition, InjectionError, InjectionResolutionError } from './Internals';
3 4
 
4 5
 class _Injector {
5 6
 
6 7
   private injectionQueue: any[] = []
7
-  private modules: { implements?: Constructor<any>, ctor: Type<any> }[] = []
8
-  private moduleObjs: { [key in string]: any } = {}
8
+  private singletonDefinitions: SingletonDefinition[] = []
9
+
10
+  private singletonObjects: { [classname in string]: any } = {}
11
+  private tokenLookupTable: { [token in string]: Constructor<any> } = {}
12
+
9 13
   private initialized = false
10 14
 
11 15
   /**
12 16
    * Resolves instances by injecting required services
13
-   * @param {Type<any>} target
17
+   * @param {Type<any>} request
14 18
    * @returns {T}
15 19
    */
16
-  public resolve<T>(target: Constructor<T>): T {
20
+  public resolve<T>(request: Constructor<T>): T {
17 21
     if (!this.initialized) {
18 22
       this.initialize()
19 23
       this.initialized = true
20 24
     }
21 25
 
22
-    return this.moduleObjs[target.name] as any
26
+    return this.singletonObjects[request.name] as any
23 27
   }
24 28
 
25 29
   public async resolveAsync<T>(target: Type<T>): Promise<T> {
26 30
     if (!this.initialized) {
27
-      await this.initialize()
31
+      this.initialize()
28 32
       this.initialized = true
29 33
     }
30 34
 
31
-    return this.moduleObjs[target.name] as any
35
+    return this.singletonObjects[target.name] as any
32 36
   }
33 37
 
34
-  private initialize = async (async?: boolean) => {
38
+  private initialize = () => {
35 39
     this.createSingletons()
36 40
     this.injectDependencies()
37
-    if (async)
38
-      await this.initializeSingletons()
39
-    else
40
-      this.initializeSingletons()
41
+    this.initializeSingletons()
42
+    this.cleanup()
41 43
   }
42 44
 
43 45
   private createSingletons = () => {
44
-    //instantiate all non-root modules
45
-    this.modules.forEach(m => {
46
-      const module = new m.ctor()
47
-      if (m.implements)
48
-        this.moduleObjs[m.implements.name] = module
49
-      this.moduleObjs[m.ctor.name] = module
46
+    this.singletonDefinitions.forEach(def => {
47
+      const obj = new def.ctor()
48
+
49
+      if (def.initializationPriority != undefined && !obj.initialize) {
50
+        throw new InjectionError(ERR_NO_INITIALIZE_WITH_PRIORITY(def.ctor))
51
+      }
52
+
53
+      this.singletonObjects[def.ctor.name] = obj
50 54
     })
51 55
   }
52 56
 
53 57
   private injectDependencies = () => {
54
-    while (this.injectionQueue.length > 0) {
55
-      const inj = this.injectionQueue.shift()
58
+    this.injectionQueue.forEach(inj => {
56 59
 
57
-      if (this.moduleObjs[inj.injectionType.name]) {
58
-        this.moduleObjs[inj.instance.constructor.name][inj.injectIntoKey] = this.moduleObjs[inj.injectionType.name]
60
+      if (inj.token.name in this.tokenLookupTable) { //injection alias was used
61
+        inj.token = this.tokenLookupTable[inj.token.name]
62
+      }
63
+
64
+      if (this.singletonObjects[inj.token.name]) {
65
+        this.singletonObjects[inj.receiver.constructor.name][inj.key] = this.singletonObjects[inj.token.name]
59 66
       } else {
60
-        throw new Error("Cannot resolve injection token " + inj.injectionType.name)
67
+        throw new InjectionResolutionError(ERR_NO_INJECTION_TOKEN(inj.injectionType))
61 68
       }
62
-    }
69
+    })
63 70
   }
64 71
 
65 72
   private initializeSingletons = () => {
66
-    Object.values(this.moduleObjs).forEach(element => element.initialize ? element.initialize() : undefined);
73
+    this.singletonDefinitions
74
+      .sort((a, b) => (a.initializationPriority ?? 0) - (b.initializationPriority ?? 0))
75
+      .map(def => this.singletonObjects[def.ctor.name])
76
+      .forEach(obj => obj.initialize ? obj.initialize() : undefined)
77
+  }
78
+
79
+  private cleanup = () => {
80
+    while (this.singletonDefinitions.length > 0)
81
+      this.singletonDefinitions.pop()
82
+    while (this.injectionQueue.length > 0)
83
+      this.injectionQueue.pop()
67 84
   }
68 85
 }
69 86
 

+ 8
- 0
src/Interfaces.ts View File

@@ -0,0 +1,8 @@
1
+
2
+export interface Initializable {
3
+    initialize: () => void
4
+}
5
+
6
+export interface AsyncInitializable {
7
+    initialize: () => void | Promise<void>
8
+}

+ 37
- 0
src/Internals.ts View File

@@ -0,0 +1,37 @@
1
+/**
2
+ * Type for what object is instances of. Also applicable to "Constructor of T" as Types/Classes/Constructors are interchangable in TS.
3
+ */
4
+export interface Type<T> {
5
+  new(...args: any[]): T;
6
+}
7
+
8
+export type Constructor<T> = Function & { prototype: T }
9
+
10
+/**
11
+ * Generic `ClassDecorator` type
12
+ */
13
+export type GenericClassDecorator<T> = (target: T) => void;
14
+
15
+export type Module = {
16
+  initializationPriority?: number //Priority of initializing this object after creation 
17
+  ctor: Type<any>                 //Object constructor to make singleton from
18
+}
19
+
20
+export class NamedError extends Error{
21
+  constructor(message: string){
22
+    super(message)
23
+    this.name = this.constructor.name
24
+  }
25
+}
26
+
27
+export class InjectionError extends NamedError{
28
+  constructor(message: string){
29
+    super(message)
30
+  }
31
+}
32
+
33
+export class InjectionResolutionError extends InjectionError{
34
+  constructor(message: string){
35
+    super(message)
36
+  }
37
+}

+ 4
- 0
src/Strings.ts View File

@@ -0,0 +1,4 @@
1
+import { Constructor } from "./Internals";
2
+
3
+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'.`
4
+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.`

+ 0
- 17
src/Types.ts View File

@@ -1,17 +0,0 @@
1
-/**
2
- * Type for what object is instances of. Also applicable to "Constructor of T" as Types/Classes/Constructors are interchangable in TS.
3
- */
4
-export interface Type<T> {
5
-  new(...args: any[]): T;
6
-}
7
-
8
-export type Constructor<T> = Function & { prototype: T }
9
-
10
-/**
11
- * Generic `ClassDecorator` type
12
- */
13
-export type GenericClassDecorator<T> = (target: T) => void;
14
-
15
-export interface ISingleton{
16
-  initialize?(): void | Promise<void>
17
-}

+ 2
- 1
test/BasicTest/ComponentA.ts View File

@@ -1,8 +1,9 @@
1 1
 import { Singleton } from "../../src/Decorator";
2
+import { Initializable } from "../../src/Interfaces";
2 3
 import { COMPONENT_A_VALUE } from "../CONSTANTS";
3 4
 
4 5
 @Singleton()
5
-export class ComponentA{
6
+export class ComponentA implements Initializable{
6 7
     private value: string
7 8
     
8 9
     getFromThis(): string {

+ 5
- 2
test/BasicTest/ComponentB.ts View File

@@ -1,4 +1,5 @@
1 1
 import { Inject, Singleton } from "../../src/Decorator"
2
+import { Initializable } from "../../src/Interfaces"
2 3
 import { COMPONENT_B_VALUE } from "../CONSTANTS"
3 4
 import { ComponentA } from "./ComponentA"
4 5
 
@@ -7,8 +8,10 @@ export abstract class IComponentB{
7 8
     getFromThis: () => string
8 9
 }
9 10
 
10
-@Singleton(IComponentB)
11
-export class ComponentB implements IComponentB{
11
+@Singleton({
12
+    interface: IComponentB
13
+})
14
+export class ComponentB implements IComponentB, Initializable{
12 15
 
13 16
     @Inject(ComponentA)
14 17
     private componentA: ComponentA

+ 28
- 0
test/BasicTest/ComponentC.ts View File

@@ -0,0 +1,28 @@
1
+import { Inject, Singleton } from "../../src/Decorator"
2
+import { Initializable } from "../../src/Interfaces"
3
+import { COMPONENT_B_VALUE, COMPONENT_C_VALUE } from "../CONSTANTS"
4
+import { ComponentA } from "./ComponentA"
5
+
6
+export abstract class IComponentC{
7
+    getFromA: () => string
8
+    getFromThis: () => string
9
+}
10
+
11
+@Singleton({
12
+    interface: IComponentC,
13
+})
14
+export class ComponentC implements IComponentC{
15
+
16
+    @Inject(ComponentA)
17
+    private componentA: ComponentA
18
+
19
+    private value: string = COMPONENT_C_VALUE
20
+
21
+    getFromA(): string {
22
+        return this.componentA.getFromThis()
23
+    }
24
+
25
+    getFromThis(): string {
26
+        return this.value
27
+    }
28
+}

+ 3
- 2
test/BasicTest/Test.ts View File

@@ -1,7 +1,7 @@
1 1
 import { Injector } from '../../src/Injector'
2 2
 import { TestComponent } from './TestComponent'
3
-import { assert, expect } from 'chai';
4
-import { COMPONENT_A_VALUE, COMPONENT_B_VALUE } from '../CONSTANTS';
3
+import { expect } from 'chai';
4
+import { COMPONENT_A_VALUE, COMPONENT_B_VALUE, COMPONENT_C_VALUE } from '../CONSTANTS';
5 5
 
6 6
 var should = require('chai').should();
7 7
 var chai = require("chai");
@@ -17,6 +17,7 @@ describe('dependjs', () => {
17 17
         expect(testComp.getFromA()).to.be.equal(COMPONENT_A_VALUE)
18 18
         expect(testComp.getAThroughB()).to.be.equal(COMPONENT_A_VALUE)
19 19
         expect(testComp.getFromB()).to.be.equal(COMPONENT_B_VALUE)
20
+        expect(testComp.getFromC()).to.be.equal(COMPONENT_C_VALUE)
20 21
 
21 22
     })
22 23
 })

+ 8
- 0
test/BasicTest/TestComponent.ts View File

@@ -1,6 +1,8 @@
1 1
 import { Inject, Singleton } from "../../src/Decorator";
2
+import { Initializable } from "../../src/Interfaces";
2 3
 import {ComponentA} from "./ComponentA"
3 4
 import {IComponentB} from "./ComponentB"
5
+import { ComponentC } from "./ComponentC";
4 6
 
5 7
 @Singleton()
6 8
 export class TestComponent{
@@ -11,6 +13,9 @@ export class TestComponent{
11 13
     @Inject(ComponentA)
12 14
     private componentA: ComponentA
13 15
 
16
+    @Inject(ComponentC)
17
+    private componentC: ComponentC
18
+
14 19
     getFromA(): string{
15 20
         return this.componentA.getFromThis()
16 21
     }
@@ -23,4 +28,7 @@ export class TestComponent{
23 28
         return this.compoenntB.getFromThis()
24 29
     }
25 30
 
31
+    getFromC(): string{
32
+        return this.componentC.getFromThis()
33
+    }
26 34
 }

+ 2
- 1
test/CONSTANTS.ts View File

@@ -1,2 +1,3 @@
1 1
 export const COMPONENT_A_VALUE = "ComponentA"
2
-export const COMPONENT_B_VALUE = "ComponentB"
2
+export const COMPONENT_B_VALUE = "ComponentB"
3
+export const COMPONENT_C_VALUE = "ComponentC"

+ 16
- 0
test/InitializationTest/ComponentA.ts View File

@@ -0,0 +1,16 @@
1
+import { Inject, Singleton } from "../../src/Decorator"
2
+import { Initializable } from "../../src/Interfaces"
3
+import { COMPONENT_A_VALUE } from "../CONSTANTS"
4
+import { TestComponent } from "./TestComponent"
5
+
6
+@Singleton({
7
+    initializationPriority: 3
8
+})
9
+export class ComponentA implements Initializable{
10
+    @Inject(TestComponent)
11
+    private testComponent: TestComponent
12
+
13
+    initialize(): void{
14
+        this.testComponent.pushData(COMPONENT_A_VALUE)
15
+    }
16
+}

+ 16
- 0
test/InitializationTest/ComponentB.ts View File

@@ -0,0 +1,16 @@
1
+import { Inject, Singleton } from "../../src/Decorator"
2
+import { Initializable } from "../../src/Interfaces"
3
+import { COMPONENT_B_VALUE } from "../CONSTANTS"
4
+import { TestComponent } from "./TestComponent"
5
+
6
+@Singleton({
7
+    initializationPriority: 2
8
+})
9
+export class ComponentB implements Initializable{
10
+    @Inject(TestComponent)
11
+    private testComponent: TestComponent
12
+
13
+    initialize(): void{
14
+        this.testComponent.pushData(COMPONENT_B_VALUE)
15
+    }
16
+}

+ 16
- 0
test/InitializationTest/ComponentC.ts View File

@@ -0,0 +1,16 @@
1
+import { Inject, Singleton } from "../../src/Decorator"
2
+import { Initializable } from "../../src/Interfaces"
3
+import { COMPONENT_C_VALUE } from "../CONSTANTS"
4
+import { TestComponent } from "./TestComponent"
5
+
6
+@Singleton({
7
+    initializationPriority: 1
8
+})
9
+export class ComponentC implements Initializable{
10
+    @Inject(TestComponent)
11
+    private testComponent: TestComponent
12
+
13
+    initialize(): void{
14
+        this.testComponent.pushData(COMPONENT_C_VALUE)
15
+    }
16
+}

+ 19
- 0
test/InitializationTest/Test.ts View File

@@ -0,0 +1,19 @@
1
+import { expect } from 'chai';
2
+import { Injector } from '../../src/Injector'
3
+import { COMPONENT_A_VALUE, COMPONENT_B_VALUE, COMPONENT_C_VALUE } from '../CONSTANTS';
4
+import { TestComponent } from './TestComponent'
5
+
6
+var chai = require("chai");
7
+var chaiAsPromised = require("chai-as-promised");
8
+
9
+chai.use(chaiAsPromised);
10
+
11
+describe('dependjs', () => {
12
+    it('initialized in the requested order', () => {
13
+
14
+        const testComp = Injector.resolve(TestComponent)
15
+        const data = testComp.getData()
16
+
17
+        expect(data).to.eql([COMPONENT_C_VALUE, COMPONENT_B_VALUE, COMPONENT_A_VALUE])
18
+    })
19
+})

+ 16
- 0
test/InitializationTest/TestComponent.ts View File

@@ -0,0 +1,16 @@
1
+import { Singleton } from "../../src/Decorator";
2
+
3
+@Singleton()
4
+export class TestComponent{
5
+
6
+    private data: string[] = []
7
+
8
+    pushData(str: string){
9
+        this.data.push(str)
10
+    }
11
+
12
+    getData(){
13
+        return this.data
14
+    }
15
+
16
+}

Loading…
Cancel
Save