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
 export * from './src/Decorator';
1
 export * from './src/Decorator';
2
 export * from './src/Injector';
2
 export * from './src/Injector';
3
-export * from './src/Types';
3
+export * from './src/Interfaces';

+ 1
- 1
package.json View File

21
     "tsc": "tsc",
21
     "tsc": "tsc",
22
     "build": "npm run clean && tsc",
22
     "build": "npm run clean && tsc",
23
     "clean": "rm -rf js",
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
   "license": "MIT",
26
   "license": "MIT",
27
   "dependencies": {
27
   "dependencies": {

+ 14
- 7
src/Decorator.ts View File

1
 import { Injector } from "./Injector";
1
 import { Injector } from "./Injector";
2
-import { Type, GenericClassDecorator, Constructor } from "./Types";
2
+import { Type, GenericClassDecorator, Constructor } from "./Internals";
3
 
3
 
4
 /**
4
 /**
5
  * @returns {GenericClassDecorator<Type<any>>}
5
  * @returns {GenericClassDecorator<Type<any>>}
6
  * @constructor
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
   return (clazz: Type<any>) => {
12
   return (clazz: Type<any>) => {
10
-    Injector['modules'].push({
11
-      implements: _interface ?? clazz,
13
+    Injector['singletonDefinitions'].push({
14
+      initializationPriority: config ?. initializationPriority,
12
       ctor: clazz
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
 import 'reflect-metadata';
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
 class _Injector {
5
 class _Injector {
5
 
6
 
6
   private injectionQueue: any[] = []
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
   private initialized = false
13
   private initialized = false
10
 
14
 
11
   /**
15
   /**
12
    * Resolves instances by injecting required services
16
    * Resolves instances by injecting required services
13
-   * @param {Type<any>} target
17
+   * @param {Type<any>} request
14
    * @returns {T}
18
    * @returns {T}
15
    */
19
    */
16
-  public resolve<T>(target: Constructor<T>): T {
20
+  public resolve<T>(request: Constructor<T>): T {
17
     if (!this.initialized) {
21
     if (!this.initialized) {
18
       this.initialize()
22
       this.initialize()
19
       this.initialized = true
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
   public async resolveAsync<T>(target: Type<T>): Promise<T> {
29
   public async resolveAsync<T>(target: Type<T>): Promise<T> {
26
     if (!this.initialized) {
30
     if (!this.initialized) {
27
-      await this.initialize()
31
+      this.initialize()
28
       this.initialized = true
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
     this.createSingletons()
39
     this.createSingletons()
36
     this.injectDependencies()
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
   private createSingletons = () => {
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
   private injectDependencies = () => {
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
       } else {
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
   private initializeSingletons = () => {
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

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

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

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

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

1
 import { Inject, Singleton } from "../../src/Decorator"
1
 import { Inject, Singleton } from "../../src/Decorator"
2
+import { Initializable } from "../../src/Interfaces"
2
 import { COMPONENT_B_VALUE } from "../CONSTANTS"
3
 import { COMPONENT_B_VALUE } from "../CONSTANTS"
3
 import { ComponentA } from "./ComponentA"
4
 import { ComponentA } from "./ComponentA"
4
 
5
 
7
     getFromThis: () => string
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
     @Inject(ComponentA)
16
     @Inject(ComponentA)
14
     private componentA: ComponentA
17
     private componentA: ComponentA

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

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
 import { Injector } from '../../src/Injector'
1
 import { Injector } from '../../src/Injector'
2
 import { TestComponent } from './TestComponent'
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
 var should = require('chai').should();
6
 var should = require('chai').should();
7
 var chai = require("chai");
7
 var chai = require("chai");
17
         expect(testComp.getFromA()).to.be.equal(COMPONENT_A_VALUE)
17
         expect(testComp.getFromA()).to.be.equal(COMPONENT_A_VALUE)
18
         expect(testComp.getAThroughB()).to.be.equal(COMPONENT_A_VALUE)
18
         expect(testComp.getAThroughB()).to.be.equal(COMPONENT_A_VALUE)
19
         expect(testComp.getFromB()).to.be.equal(COMPONENT_B_VALUE)
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
 import { Inject, Singleton } from "../../src/Decorator";
1
 import { Inject, Singleton } from "../../src/Decorator";
2
+import { Initializable } from "../../src/Interfaces";
2
 import {ComponentA} from "./ComponentA"
3
 import {ComponentA} from "./ComponentA"
3
 import {IComponentB} from "./ComponentB"
4
 import {IComponentB} from "./ComponentB"
5
+import { ComponentC } from "./ComponentC";
4
 
6
 
5
 @Singleton()
7
 @Singleton()
6
 export class TestComponent{
8
 export class TestComponent{
11
     @Inject(ComponentA)
13
     @Inject(ComponentA)
12
     private componentA: ComponentA
14
     private componentA: ComponentA
13
 
15
 
16
+    @Inject(ComponentC)
17
+    private componentC: ComponentC
18
+
14
     getFromA(): string{
19
     getFromA(): string{
15
         return this.componentA.getFromThis()
20
         return this.componentA.getFromThis()
16
     }
21
     }
23
         return this.compoenntB.getFromThis()
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
 export const COMPONENT_A_VALUE = "ComponentA"
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

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

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

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

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

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