diff --git a/src/helper/hook-helper.ts b/src/helper/hook-helper.ts index dd13baa..bd8a922 100644 --- a/src/helper/hook-helper.ts +++ b/src/helper/hook-helper.ts @@ -203,7 +203,7 @@ export function createHookedFunction( }, setArgs: (_args: Args) => { if (!_inThisHook) { - throw new Error('不允许在Hook作用时间结束后设置参数'); + throw new Error('It is not allowed to set the parameters after the Hook effect time is over'); } args = _args; }, @@ -237,7 +237,7 @@ export function createHookedFunction( }, setResult: (_ret: Result) => { if (!_inThisHook) { - throw new Error('不允许在Hook作用时间结束后设置返回值'); + throw new Error('It is not allowed to set the return value after the Hook effect time is over'); } ret = _ret; }, @@ -478,9 +478,10 @@ export class HookStore implements IHookStore { if (!instanceHooks.has(hook.method)) { return; } - const index = instanceHooks.get(hook.method)!.indexOf(hook); + const methodHooks = instanceHooks.get(hook.method)!; + const index = methodHooks.indexOf(hook); if (index > -1) { - instanceHooks.get(hook.method)!.splice(index, 1); + methodHooks.splice(index, 1); } } } diff --git a/src/helper/injector-helper.ts b/src/helper/injector-helper.ts index 6a6eaf0..99aa5d6 100644 --- a/src/helper/injector-helper.ts +++ b/src/helper/injector-helper.ts @@ -39,3 +39,12 @@ let index = 0; export function createId(name: string) { return `${name}_${index++}`; } + +export function createIdFactory(name: string) { + let idx = 0; + return { + create() { + return `${name}_${idx++}`; + }, + }; +} diff --git a/src/injector.ts b/src/injector.ts index f1cfa99..568469d 100644 --- a/src/injector.ts +++ b/src/injector.ts @@ -34,7 +34,7 @@ import { EventEmitter } from './helper/event'; export class Injector { id = Helper.createId('Injector'); - creatorMap = new Map(); + depth = 0; tag?: string; hookStore: IHookStore; @@ -42,6 +42,9 @@ export class Injector { private tagMatrix = new Map>(); private domainMap = new Map(); + creatorMap = new Map(); + instanceRefMap = new Map(); + private opts: InjectorOpts; constructor(providers: Provider[] = [], opts: InjectorOpts = {}, parent?: Injector) { @@ -131,18 +134,18 @@ export class Injector { } } } else { - // 首先用 Tag 去置换 Token 进行对象实例化 + // firstly, use tag to exchange token if (opts && Helper.hasTag(opts)) { const tagToken = this.exchangeToken(token, opts.tag); [creator, injector] = this.getCreator(tagToken); } - // if there is no Creator, 就从单纯的 Token 去查找创建器 + // if there is no Creator, then use the token to find the Creator if (!creator) { [creator, injector] = this.getCreator(token); } - // 非严格模式下,会自动在 get 的时候去解析依赖和 provider + // if in non-strict mode, parse dependencies and providers automatically when get if (isTypeProvider(token) && !creator && !this.opts.strict) { this.parseDependencies(token); [creator, injector] = this.getCreator(token); @@ -182,6 +185,9 @@ export class Injector { return tokens.map((token) => this.get(token)); } + /** + * Only check this injector whether has the singleton instance. + */ hasInstance(instance: any) { for (const creator of this.creatorMap.values()) { if (creator.instance === instance) { @@ -222,7 +228,7 @@ export class Injector { this.setProviders(defaultProviders, { deep: true }); - // 确保所有的依赖都有对应的 Provider + // make sure all dependencies have corresponding providers const notProvidedDeps = allDeps.filter((d) => !this.getCreator(d)[0]); if (notProvidedDeps.length) { throw InjectorError.noProviderError(...notProvidedDeps); @@ -250,7 +256,7 @@ export class Injector { return this.hookStore.createOneHook(hook); } - private instanceDisposedEmitter = new EventEmitter(); + private instanceDisposedEmitter = new EventEmitter(); onceInstanceDisposed(instance: any, cb: () => void) { const instanceId = this.getInstanceId(instance); @@ -268,6 +274,8 @@ export class Injector { const instance = creator.instance; const instanceId = this.getInstanceId(instance); + this.instanceRefMap.delete(instance); + let maybePromise: Promise | undefined; if (instance && typeof instance[key] === 'function') { maybePromise = instance[key](); @@ -278,10 +286,10 @@ export class Injector { if (maybePromise && Helper.isPromiseLike(maybePromise)) { maybePromise = maybePromise.then(() => { - this.instanceDisposedEmitter.emit(instanceId); + instanceId && this.instanceDisposedEmitter.emit(instanceId); }); } else { - this.instanceDisposedEmitter.emit(instanceId); + instanceId && this.instanceDisposedEmitter.emit(instanceId); } return maybePromise; } @@ -372,6 +380,11 @@ export class Injector { } } + private instanceIdGenerator = Helper.createIdFactory('Instance'); + private getNextInstanceId() { + return this.instanceIdGenerator.create(); + } + private resolveToken(token: Token): [Token, InstanceCreator | undefined] { let creator = this.creatorMap.get(token); @@ -404,6 +417,14 @@ export class Injector { return [null, this]; } + private getOrSaveInstanceId(instance: any, id: string) { + if (this.instanceRefMap.has(instance)) { + return this.instanceRefMap.get(instance)!; + } + this.instanceRefMap.set(instance, id); + return id; + } + private createInstance(ctx: Context, defaultOpts?: InstanceOpts, args?: any[]) { const { creator, token } = ctx; @@ -411,9 +432,9 @@ export class Injector { throw InjectorError.tagOnlyError(String(creator.tag), String(this.tag)); } - // ClassCreator 的时候,需要进行多例状态判断 if (Helper.isClassCreator(creator)) { const opts = defaultOpts ?? creator.opts; + // if a class creator is singleton, and the instance is already created, return the instance. if (!opts.multiple && creator.status === CreatorStatus.done) { return creator.instance; } @@ -444,7 +465,9 @@ export class Injector { try { const args = defaultArgs ?? this.getParameters(creator.parameters, ctx); - const instance = this.createInstanceWithInjector(cls, token, injector, args); + const nextId = this.getNextInstanceId(); + const instance = this.createInstanceWithInjector(cls, token, injector, args, nextId); + void this.getOrSaveInstanceId(instance, nextId); creator.status = CreatorStatus.init; // if not allow multiple, save the instance in creator. @@ -485,17 +508,23 @@ export class Injector { }); } - private createInstanceWithInjector(cls: ConstructorOf, token: Token, injector: Injector, args: any[]) { - // 在创建对象的过程中,先把 injector 挂载到 prototype 上,让构造函数能够访问 - // 创建完实例之后从 prototype 上去掉 injector,防止内存泄露 + private createInstanceWithInjector( + cls: ConstructorOf, + token: Token, + injector: Injector, + args: any[], + id: string, + ) { + // when creating an instance, set injector to prototype, so that the constructor can access it. + // after creating the instance, remove the injector from prototype to prevent memory leaks. setInjector(cls.prototype, injector); const ret = new cls(...args); removeInjector(cls.prototype); - // 在实例上挂载 injector,让以后的对象内部都能访问到 injector + // mount injector on the instance, so that the inner object can access the injector in the future. setInjector(ret, injector); Object.assign(ret, { - __id: Helper.createId('Instance'), + __id: id, __injectorId: injector.id, }); @@ -503,6 +532,6 @@ export class Injector { } getInstanceId(instance: any) { - return instance && instance.__id; + return this.instanceRefMap.get(instance); } } diff --git a/test/decorators/hooks.test.ts b/test/decorators/hooks.test.ts new file mode 100644 index 0000000..ab71c08 --- /dev/null +++ b/test/decorators/hooks.test.ts @@ -0,0 +1,535 @@ +import { + After, + AfterReturning, + AfterThrowing, + Around, + Aspect, + Autowired, + Before, + HookType, + IAfterJoinPoint, + IAfterReturningJoinPoint, + IAfterThrowingJoinPoint, + IAroundJoinPoint, + IBeforeJoinPoint, + Injectable, + Injector, +} from '../../src'; + +describe('hook', () => { + it('使用代码来创建hook', async () => { + const injector = new Injector(); + @Injectable() + class TestClass { + add(a: number, b: number): number { + return a + b; + } + } + + @Injectable() + class TestClass1 { + add(a: number, b: number): number { + return a + b; + } + } + + @Injectable() + class TestClass2 { + add(a: number, b: number): number { + return a + b; + } + } + + @Injectable() + class TestClass3 { + add(a: number, b: number): number { + return a + b; + } + } + + @Injectable() + class TestClass4 { + async add(a: number, b: number): Promise { + return a + b; + } + } + + @Injectable() + class TestClass5 { + async add(a: number, b: number): Promise { + return a + b; + } + } + + injector.createHook({ + hook: (joinPoint: IBeforeJoinPoint) => { + const [a, b] = joinPoint.getArgs(); + joinPoint.setArgs([a + 1, b + 1]); + }, + method: 'add', + target: TestClass, + type: HookType.Before, + }); + injector.createHook({ + hook: () => undefined, + method: 'add', + target: TestClass, + type: 'other' as any, // 不会造成任何影响(为了提高覆盖率) + }); + const testClass = injector.get(TestClass); + expect(testClass.add(1, 2)).toBe(5); + expect(testClass.add(3, 4)).toBe(9); + + // 同步变成异步 + // Async hook on sync target + injector.createHook({ + awaitPromise: true, + hook: async (joinPoint: IBeforeJoinPoint) => { + const [a, b] = joinPoint.getArgs(); + await new Promise((resolve) => { + setTimeout(resolve, 1000); + }); + joinPoint.setArgs([a + 1, b + 1]); + }, + method: 'add', + target: TestClass1, + type: HookType.Before, + }); + injector.createHook({ + hook: (joinPoint: IBeforeJoinPoint) => { + const [a, b] = joinPoint.getArgs(); + joinPoint.setArgs([a + 2, b + 2]); + }, + method: 'add', + target: TestClass1, + type: HookType.Before, + }); + const testClass1 = injector.get(TestClass1); + const ret = testClass1.add(1, 2); + expect(ret).toBeInstanceOf(Promise); + expect(await ret).toBe(9); + + injector.createHook({ + hook: async (joinPoint) => { + const result = joinPoint.getResult(); + joinPoint.setResult(result + 1); + }, + method: 'add', + target: TestClass2, + type: HookType.After, + }); + const testClass2 = injector.get(TestClass2); + expect(testClass2.add(1, 2)).toBe(4); + + injector.createHooks([ + { + hook: (joinPoint) => { + joinPoint.proceed(); + const result = joinPoint.getResult(); + if (result === 3) { + return joinPoint.setResult(10); + } + }, + method: 'add', + target: TestClass3, + type: HookType.Around, + }, + ]); + const testClass3 = injector.get(TestClass3); + expect(testClass3.add(1, 2)).toBe(10); + expect(testClass3.add(1, 3)).toBe(4); + + // Async hook on async target + injector.createHooks([ + { + hook: async (joinPoint) => { + joinPoint.proceed(); + const result = await joinPoint.getResult(); + if (result === 3) { + return joinPoint.setResult(10); + } + }, + method: 'add', + target: TestClass4, + type: HookType.Around, + }, + ]); + const testClass4 = injector.get(TestClass4); + expect(await testClass4.add(1, 2)).toBe(10); + + // Sync hook on async target + injector.createHook({ + hook: async (joinPoint) => { + const [a, b] = joinPoint.getArgs(); + joinPoint.setArgs([a + 1, b + 1]); + joinPoint.proceed(); + }, + method: 'add', + target: TestClass5, + type: HookType.Around, + }); + const testClass5 = injector.get(TestClass5); + expect(await testClass5.add(1, 2)).toBe(5); + }); + + it('使用注解来创建hook', async () => { + const TestClassToken = Symbol(); + + const pendings: Array> = []; + @Injectable() + class TestClass { + exp = 1; + + add(a: number, b: number): number { + return a + b; + } + + addPromise(a: number, b: number): number { + return a + b; + } + + minus(a: number, b: number): number { + return a - b; + } + + multiple(a: number, b: number): number { + this.exp *= a * b; + return a * b; + } + + throwError(): void { + throw new Error('testError'); + } + + throwError2(): void { + throw new Error('testError2'); + } + + async throwRejection(): Promise { + throw new Error('testRejection'); + } + + // 测试内部方法调用是否成功被拦截 + anotherAdd(a: number, b: number) { + return this.add(a, b); + } + + // 不会被成功拦截 + bindedAdd = (a: number, b: number) => { + return this.add(a, b); + }; + } + + @Aspect() + @Injectable() + class TestAspect { + record = 2; + + multipleTime = 0; + + thrownError: any; + + thrownRejection: any; + + @Before(TestClass, 'add') + interceptAdd(joinPoint: IBeforeJoinPoint) { + const [a, b] = joinPoint.getArgs(); + expect(joinPoint.getMethodName()).toBe('add'); + expect(joinPoint.getOriginalArgs()).toBeInstanceOf(Array); + expect(joinPoint.getThis()).toBeInstanceOf(TestClass); + joinPoint.setArgs([a * 10, b * 10]); + pendings.push( + new Promise((resolve, reject) => { + setTimeout(() => { + try { + expect(() => joinPoint.setArgs([1, 0])).toThrowError(); + resolve(); + } catch (e) { + reject(e); + } + }, 100); + }), + ); + } + + @Before>(TestClass, 'addPromise') + interceptAddPromise(joinPoint: IBeforeJoinPoint>) { + const [a, b] = joinPoint.getArgs(); + expect(joinPoint.getMethodName()).toBe('addPromise'); + expect(joinPoint.getOriginalArgs()).toBeInstanceOf(Array); + expect(joinPoint.getThis()).toBeInstanceOf(TestClass); + joinPoint.setArgs([a * 10, b * 10]); + } + + @After>(TestClass, 'addPromise', { await: true }) + async interceptAddPromiseAfter(joinPoint: IAfterJoinPoint>) { + const result = await joinPoint.getResult(); + expect(joinPoint.getMethodName()).toBe('addPromise'); + expect(joinPoint.getOriginalArgs()).toBeInstanceOf(Array); + expect(joinPoint.getArgs()).toBeInstanceOf(Array); + expect(joinPoint.getThis()).toBeInstanceOf(TestClass); + await new Promise((resolve) => { + setTimeout(resolve, 1000); + }); + joinPoint.setResult(Promise.resolve(result * 5)); + } + + @After(TestClass, 'minus') + interceptMinus(joinPoint: IAfterJoinPoint) { + expect(joinPoint.getMethodName()).toBe('minus'); + expect(joinPoint.getOriginalArgs()).toBeInstanceOf(Array); + expect(joinPoint.getThis()).toBeInstanceOf(TestClass); + const result = joinPoint.getResult(); + joinPoint.setResult(result * 20); + pendings.push( + new Promise((resolve, reject) => { + setTimeout(() => { + try { + expect(() => joinPoint.setResult(100)).toThrowError(); + resolve(); + } catch (e) { + reject(e); + } + }, 100); + }), + ); + } + + @Around(TestClass, 'multiple') + interceptMultiple(joinPoint: IAroundJoinPoint) { + expect(joinPoint.getMethodName()).toBe('multiple'); + expect(joinPoint.getOriginalArgs()).toBeInstanceOf(Array); + expect(joinPoint.getThis()).toBeInstanceOf(TestClass); + joinPoint.proceed(); + const result = joinPoint.getResult(); + this.record *= result; + joinPoint.setResult(this.record); + } + + @AfterReturning(TestClass, 'multiple') + afterMultiple(joinPoint: IAfterReturningJoinPoint) { + expect(joinPoint.getMethodName()).toBe('multiple'); + expect(joinPoint.getOriginalArgs()).toBeInstanceOf(Array); + expect(joinPoint.getArgs()).toBeInstanceOf(Array); + expect(joinPoint.getThis()).toBeInstanceOf(TestClass); + expect(joinPoint.getResult()).toBe(this.record); + this.multipleTime++; + } + + @AfterReturning(TestClass, 'multiple') + afterMultipleButThrowError(joinPoint: IAfterReturningJoinPoint) { + throw new Error('error in AfterReturning'); + } + + @Before(TestClassToken, 'add') + interceptAddByToken(joinPoint: IBeforeJoinPoint) { + expect(joinPoint.getMethodName()).toBe('add'); + expect(joinPoint.getOriginalArgs()).toBeInstanceOf(Array); + expect(joinPoint.getThis()).toBeInstanceOf(TestClass); + const [a, b] = joinPoint.getArgs(); + joinPoint.setArgs([a * 20, b * 20]); + } + + @AfterThrowing(TestClass, 'throwError') + afterThrowError(joinPoint: IAfterThrowingJoinPoint) { + expect(joinPoint.getMethodName()).toBe('throwError'); + expect(joinPoint.getOriginalArgs()).toBeInstanceOf(Array); + expect(joinPoint.getThis()).toBeInstanceOf(TestClass); + this.thrownError = joinPoint.getError(); + } + + @Before(TestClass, 'throwError2') + beforeThrowError2(joinPoint: IBeforeJoinPoint) { + expect(joinPoint.getArgs().length).toBe(0); + return; + } + + @AfterThrowing(TestClass, 'throwRejection') + afterThrowRejection(joinPoint: IAfterThrowingJoinPoint) { + expect(joinPoint.getMethodName()).toBe('throwRejection'); + expect(joinPoint.getOriginalArgs()).toBeInstanceOf(Array); + expect(joinPoint.getThis()).toBeInstanceOf(TestClass); + this.thrownRejection = joinPoint.getError(); + } + } + + @Aspect() + @Injectable() + class EmptyTestAspect {} + + const injector = new Injector(); + injector.addProviders({ token: TestClassToken, useClass: TestClass }); + injector.addProviders(TestAspect); + injector.addProviders(EmptyTestAspect); + const testClass = injector.get(TestClass); + const testClassByToken = injector.get(TestClassToken); + const aspector = injector.get(TestAspect); + expect(testClass.add(1, 2)).toBe(30); + expect(testClass.anotherAdd(1, 2)).toBe(30); + expect(testClass.bindedAdd(1, 2)).toBe(3); + expect(await testClass.addPromise(1, 2)).toBe(150); + expect(testClassByToken.add(1, 2)).toBe(60); + expect(testClassByToken.anotherAdd(1, 2)).toBe(60); + expect(testClass.minus(2, 9)).toBe(-140); + expect(testClass.multiple(1, 2)).toBe(4); + expect(aspector.multipleTime).toBe(1); + expect(injector.get(TestAspect).record).toBe(4); + expect(testClass.exp).toBe(2); + expect(testClass.multiple(3, 4)).toBe(48); + expect(injector.get(TestAspect).record).toBe(48); + expect(testClass.exp).toBe(24); + + expect(() => testClass.throwError()).toThrowError(aspector.thrownError); + let rejected = false; + try { + await testClass.throwRejection(); + } catch (e) { + rejected = true; + expect(aspector.thrownRejection).toBe(e); + } + + expect(rejected).toBeTruthy(); + expect(() => testClass.throwError2()).toThrowError(); + + await Promise.all(pendings); + }); + + it('子injector应该正确拦截', () => { + @Injectable() + class TestClass { + add(a: number, b: number): number { + return a + b; + } + + minus(a: number, b: number): number { + return a - b; + } + + multiple(a: number, b: number): number { + return a * b; + } + } + + @Injectable() + class TestClass2 { + add(a: number, b: number): number { + return a + b; + } + + minus(a: number, b: number): number { + return a - b; + } + + multiple(a: number, b: number): number { + return a * b; + } + } + @Aspect() + @Injectable() + class TestAspect { + @Before(TestClass, 'add') + interceptAdd(joinPoint: IBeforeJoinPoint) { + const [a, b] = joinPoint.getArgs(); + joinPoint.setArgs([a * 10, b * 10]); + } + + @Before(TestClass2, 'add') + interceptAdd2(joinPoint: IBeforeJoinPoint) { + const [a, b] = joinPoint.getArgs(); + joinPoint.setArgs([a * 10, b * 10]); + } + } + + @Aspect() + @Injectable() + class TestAspect2 { + @Before(TestClass, 'add') + interceptAdd(joinPoint: IBeforeJoinPoint) { + const [a, b] = joinPoint.getArgs(); + joinPoint.setArgs([a * 20, b * 20]); + } + @Before(TestClass2, 'add') + interceptAdd2(joinPoint: IBeforeJoinPoint) { + const [a, b] = joinPoint.getArgs(); + joinPoint.setArgs([a * 20, b * 20]); + } + } + + const injector = new Injector([TestClass], { strict: true }); + const injector2 = injector.createChild(); + injector.addProviders(TestAspect); + injector2.addProviders(TestAspect2); + injector2.addProviders(TestClass2); + const testClass = injector.get(TestClass); + const testClassChild = injector2.get(TestClass); + const testClassChild2 = injector2.get(TestClass2); + expect(testClass.add(1, 2)).toBe(30); + expect(testClassChild.add(1, 2)).toBe(30); // 仅仅命中parent中的Hook + expect(testClassChild2.add(1, 2)).toBe(600); // 两边的hook都会命中 + }); + + it('should dispose hook', () => { + const constructorSpy = jest.fn(); + + @Injectable() + class TestClass { + exp = 1; + + constructor() { + constructorSpy(); + } + + add(a: number, b: number): number { + return a + b; + } + } + + @Aspect() + @Injectable() + class TestAspect { + record = 2; + + multipleTime = 0; + + thrownError: any; + + thrownRejection: any; + + @Before(TestClass, 'add') + interceptAdd(joinPoint: IBeforeJoinPoint) { + const [a, b] = joinPoint.getArgs(); + expect(joinPoint.getMethodName()).toBe('add'); + expect(joinPoint.getOriginalArgs()).toBeInstanceOf(Array); + expect(joinPoint.getThis()).toBeInstanceOf(TestClass); + joinPoint.setArgs([a * 10, b * 10]); + } + } + + @Injectable() + class Example { + @Autowired() + testClass!: TestClass; + + run() { + return this.testClass.add(1, 2); + } + } + + const injector = new Injector(); + injector.addProviders(TestClass); + injector.addProviders(TestAspect); + const testClass = injector.get(TestClass); + expect(testClass.add(1, 2)).toBe(30); + + const example = injector.get(Example); + expect(example.run()).toBe(30); + + expect(constructorSpy).toBeCalledTimes(1); + + injector.disposeOne(TestClass); + // injector.disposeOne(TestAspect); + + expect(example.run()).toBe(30); + expect(constructorSpy).toBeCalledTimes(2); + }); +}); diff --git a/test/injector.test.ts b/test/injector.test.ts index 9c03033..90e0522 100644 --- a/test/injector.test.ts +++ b/test/injector.test.ts @@ -431,463 +431,12 @@ describe('test injector work', () => { expect((instance1 as any).__id.startsWith('Instance')).toBeTruthy(); expect((instance1 as any).__id).toBe((instance2 as any).__id); + expect((instance1 as any).__id).toBe((injector as any).getOrSaveInstanceId(instance1)); + expect((instance2 as any).__id).toBe((injector as any).getOrSaveInstanceId(instance2)); expect((instance1 as any).__id).not.toBe((instance3 as any).__id); }); }); - describe('hook', () => { - it('使用代码来创建hook', async () => { - const injector = new Injector(); - @Injectable() - class TestClass { - add(a: number, b: number): number { - return a + b; - } - } - - @Injectable() - class TestClass1 { - add(a: number, b: number): number { - return a + b; - } - } - - @Injectable() - class TestClass2 { - add(a: number, b: number): number { - return a + b; - } - } - - @Injectable() - class TestClass3 { - add(a: number, b: number): number { - return a + b; - } - } - - @Injectable() - class TestClass4 { - async add(a: number, b: number): Promise { - return a + b; - } - } - - @Injectable() - class TestClass5 { - async add(a: number, b: number): Promise { - return a + b; - } - } - - injector.createHook({ - hook: (joinPoint: IBeforeJoinPoint) => { - const [a, b] = joinPoint.getArgs(); - joinPoint.setArgs([a + 1, b + 1]); - }, - method: 'add', - target: TestClass, - type: HookType.Before, - }); - injector.createHook({ - hook: () => undefined, - method: 'add', - target: TestClass, - type: 'other' as any, // 不会造成任何影响(为了提高覆盖率) - }); - const testClass = injector.get(TestClass); - expect(testClass.add(1, 2)).toBe(5); - expect(testClass.add(3, 4)).toBe(9); - - // 同步变成异步 - // Async hook on sync target - injector.createHook({ - awaitPromise: true, - hook: async (joinPoint: IBeforeJoinPoint) => { - const [a, b] = joinPoint.getArgs(); - await new Promise((resolve) => { - setTimeout(resolve, 1000); - }); - joinPoint.setArgs([a + 1, b + 1]); - }, - method: 'add', - target: TestClass1, - type: HookType.Before, - }); - injector.createHook({ - hook: (joinPoint: IBeforeJoinPoint) => { - const [a, b] = joinPoint.getArgs(); - joinPoint.setArgs([a + 2, b + 2]); - }, - method: 'add', - target: TestClass1, - type: HookType.Before, - }); - const testClass1 = injector.get(TestClass1); - const ret = testClass1.add(1, 2); - expect(ret).toBeInstanceOf(Promise); - expect(await ret).toBe(9); - - injector.createHook({ - hook: async (joinPoint) => { - const result = joinPoint.getResult(); - joinPoint.setResult(result + 1); - }, - method: 'add', - target: TestClass2, - type: HookType.After, - }); - const testClass2 = injector.get(TestClass2); - expect(testClass2.add(1, 2)).toBe(4); - - injector.createHooks([ - { - hook: (joinPoint) => { - joinPoint.proceed(); - const result = joinPoint.getResult(); - if (result === 3) { - return joinPoint.setResult(10); - } - }, - method: 'add', - target: TestClass3, - type: HookType.Around, - }, - ]); - const testClass3 = injector.get(TestClass3); - expect(testClass3.add(1, 2)).toBe(10); - expect(testClass3.add(1, 3)).toBe(4); - - // Async hook on async target - injector.createHooks([ - { - hook: async (joinPoint) => { - joinPoint.proceed(); - const result = await joinPoint.getResult(); - if (result === 3) { - return joinPoint.setResult(10); - } - }, - method: 'add', - target: TestClass4, - type: HookType.Around, - }, - ]); - const testClass4 = injector.get(TestClass4); - expect(await testClass4.add(1, 2)).toBe(10); - - // Sync hook on async target - injector.createHook({ - hook: async (joinPoint) => { - const [a, b] = joinPoint.getArgs(); - joinPoint.setArgs([a + 1, b + 1]); - joinPoint.proceed(); - }, - method: 'add', - target: TestClass5, - type: HookType.Around, - }); - const testClass5 = injector.get(TestClass5); - expect(await testClass5.add(1, 2)).toBe(5); - }); - - it('使用注解来创建hook', async () => { - const TestClassToken = Symbol(); - - const pendings: Array> = []; - @Injectable() - class TestClass { - exp = 1; - - add(a: number, b: number): number { - return a + b; - } - - addPromise(a: number, b: number): number { - return a + b; - } - - minus(a: number, b: number): number { - return a - b; - } - - multiple(a: number, b: number): number { - this.exp *= a * b; - return a * b; - } - - throwError(): void { - throw new Error('testError'); - } - - throwError2(): void { - throw new Error('testError2'); - } - - async throwRejection(): Promise { - throw new Error('testRejection'); - } - - // 测试内部方法调用是否成功被拦截 - anotherAdd(a: number, b: number) { - return this.add(a, b); - } - - // 不会被成功拦截 - bindedAdd = (a: number, b: number) => { - return this.add(a, b); - }; - } - - @Aspect() - @Injectable() - class TestAspect { - record = 2; - - multipleTime = 0; - - thrownError: any; - - thrownRejection: any; - - @Before(TestClass, 'add') - interceptAdd(joinPoint: IBeforeJoinPoint) { - const [a, b] = joinPoint.getArgs(); - expect(joinPoint.getMethodName()).toBe('add'); - expect(joinPoint.getOriginalArgs()).toBeInstanceOf(Array); - expect(joinPoint.getThis()).toBeInstanceOf(TestClass); - joinPoint.setArgs([a * 10, b * 10]); - pendings.push( - new Promise((resolve, reject) => { - setTimeout(() => { - try { - expect(() => joinPoint.setArgs([1, 0])).toThrowError(); - resolve(); - } catch (e) { - reject(e); - } - }, 100); - }), - ); - } - - @Before>(TestClass, 'addPromise') - interceptAddPromise(joinPoint: IBeforeJoinPoint>) { - const [a, b] = joinPoint.getArgs(); - expect(joinPoint.getMethodName()).toBe('addPromise'); - expect(joinPoint.getOriginalArgs()).toBeInstanceOf(Array); - expect(joinPoint.getThis()).toBeInstanceOf(TestClass); - joinPoint.setArgs([a * 10, b * 10]); - } - - @After>(TestClass, 'addPromise', { await: true }) - async interceptAddPromiseAfter(joinPoint: IAfterJoinPoint>) { - const result = await joinPoint.getResult(); - expect(joinPoint.getMethodName()).toBe('addPromise'); - expect(joinPoint.getOriginalArgs()).toBeInstanceOf(Array); - expect(joinPoint.getArgs()).toBeInstanceOf(Array); - expect(joinPoint.getThis()).toBeInstanceOf(TestClass); - await new Promise((resolve) => { - setTimeout(resolve, 1000); - }); - joinPoint.setResult(Promise.resolve(result * 5)); - } - - @After(TestClass, 'minus') - interceptMinus(joinPoint: IAfterJoinPoint) { - expect(joinPoint.getMethodName()).toBe('minus'); - expect(joinPoint.getOriginalArgs()).toBeInstanceOf(Array); - expect(joinPoint.getThis()).toBeInstanceOf(TestClass); - const result = joinPoint.getResult(); - joinPoint.setResult(result * 20); - pendings.push( - new Promise((resolve, reject) => { - setTimeout(() => { - try { - expect(() => joinPoint.setResult(100)).toThrowError(); - resolve(); - } catch (e) { - reject(e); - } - }, 100); - }), - ); - } - - @Around(TestClass, 'multiple') - interceptMultiple(joinPoint: IAroundJoinPoint) { - expect(joinPoint.getMethodName()).toBe('multiple'); - expect(joinPoint.getOriginalArgs()).toBeInstanceOf(Array); - expect(joinPoint.getThis()).toBeInstanceOf(TestClass); - joinPoint.proceed(); - const result = joinPoint.getResult(); - this.record *= result; - joinPoint.setResult(this.record); - } - - @AfterReturning(TestClass, 'multiple') - afterMultiple(joinPoint: IAfterReturningJoinPoint) { - expect(joinPoint.getMethodName()).toBe('multiple'); - expect(joinPoint.getOriginalArgs()).toBeInstanceOf(Array); - expect(joinPoint.getArgs()).toBeInstanceOf(Array); - expect(joinPoint.getThis()).toBeInstanceOf(TestClass); - expect(joinPoint.getResult()).toBe(this.record); - this.multipleTime++; - } - - @AfterReturning(TestClass, 'multiple') - afterMultipleButThrowError(joinPoint: IAfterReturningJoinPoint) { - throw new Error('error in AfterReturning'); - } - - @Before(TestClassToken, 'add') - interceptAddByToken(joinPoint: IBeforeJoinPoint) { - expect(joinPoint.getMethodName()).toBe('add'); - expect(joinPoint.getOriginalArgs()).toBeInstanceOf(Array); - expect(joinPoint.getThis()).toBeInstanceOf(TestClass); - const [a, b] = joinPoint.getArgs(); - joinPoint.setArgs([a * 20, b * 20]); - } - - @AfterThrowing(TestClass, 'throwError') - afterThrowError(joinPoint: IAfterThrowingJoinPoint) { - expect(joinPoint.getMethodName()).toBe('throwError'); - expect(joinPoint.getOriginalArgs()).toBeInstanceOf(Array); - expect(joinPoint.getThis()).toBeInstanceOf(TestClass); - this.thrownError = joinPoint.getError(); - } - - @Before(TestClass, 'throwError2') - beforeThrowError2(joinPoint: IBeforeJoinPoint) { - expect(joinPoint.getArgs().length).toBe(0); - return; - } - - @AfterThrowing(TestClass, 'throwRejection') - afterThrowRejection(joinPoint: IAfterThrowingJoinPoint) { - expect(joinPoint.getMethodName()).toBe('throwRejection'); - expect(joinPoint.getOriginalArgs()).toBeInstanceOf(Array); - expect(joinPoint.getThis()).toBeInstanceOf(TestClass); - this.thrownRejection = joinPoint.getError(); - } - } - - @Aspect() - @Injectable() - class EmptyTestAspect {} - - const injector = new Injector(); - injector.addProviders({ token: TestClassToken, useClass: TestClass }); - injector.addProviders(TestAspect); - injector.addProviders(EmptyTestAspect); - const testClass = injector.get(TestClass); - const testClassByToken = injector.get(TestClassToken); - const aspector = injector.get(TestAspect); - expect(testClass.add(1, 2)).toBe(30); - expect(testClass.anotherAdd(1, 2)).toBe(30); - expect(testClass.bindedAdd(1, 2)).toBe(3); - expect(await testClass.addPromise(1, 2)).toBe(150); - expect(testClassByToken.add(1, 2)).toBe(60); - expect(testClassByToken.anotherAdd(1, 2)).toBe(60); - expect(testClass.minus(2, 9)).toBe(-140); - expect(testClass.multiple(1, 2)).toBe(4); - expect(aspector.multipleTime).toBe(1); - expect(injector.get(TestAspect).record).toBe(4); - expect(testClass.exp).toBe(2); - expect(testClass.multiple(3, 4)).toBe(48); - expect(injector.get(TestAspect).record).toBe(48); - expect(testClass.exp).toBe(24); - - expect(() => testClass.throwError()).toThrowError(aspector.thrownError); - let rejected = false; - try { - await testClass.throwRejection(); - } catch (e) { - rejected = true; - expect(aspector.thrownRejection).toBe(e); - } - - expect(rejected).toBeTruthy(); - expect(() => testClass.throwError2()).toThrowError(); - - await Promise.all(pendings); - }); - - it('子injector应该正确拦截', () => { - @Injectable() - class TestClass { - add(a: number, b: number): number { - return a + b; - } - - minus(a: number, b: number): number { - return a - b; - } - - multiple(a: number, b: number): number { - return a * b; - } - } - - @Injectable() - class TestClass2 { - add(a: number, b: number): number { - return a + b; - } - - minus(a: number, b: number): number { - return a - b; - } - - multiple(a: number, b: number): number { - return a * b; - } - } - @Aspect() - @Injectable() - class TestAspect { - @Before(TestClass, 'add') - interceptAdd(joinPoint: IBeforeJoinPoint) { - const [a, b] = joinPoint.getArgs(); - joinPoint.setArgs([a * 10, b * 10]); - } - - @Before(TestClass2, 'add') - interceptAdd2(joinPoint: IBeforeJoinPoint) { - const [a, b] = joinPoint.getArgs(); - joinPoint.setArgs([a * 10, b * 10]); - } - } - - @Aspect() - @Injectable() - class TestAspect2 { - @Before(TestClass, 'add') - interceptAdd(joinPoint: IBeforeJoinPoint) { - const [a, b] = joinPoint.getArgs(); - joinPoint.setArgs([a * 20, b * 20]); - } - @Before(TestClass2, 'add') - interceptAdd2(joinPoint: IBeforeJoinPoint) { - const [a, b] = joinPoint.getArgs(); - joinPoint.setArgs([a * 20, b * 20]); - } - } - - const injector = new Injector([TestClass], { strict: true }); - const injector2 = injector.createChild(); - injector.addProviders(TestAspect); - injector2.addProviders(TestAspect2); - injector2.addProviders(TestClass2); - const testClass = injector.get(TestClass); - const testClassChild = injector2.get(TestClass); - const testClassChild2 = injector2.get(TestClass2); - expect(testClass.add(1, 2)).toBe(30); - expect(testClassChild.add(1, 2)).toBe(30); // 仅仅命中parent中的Hook - expect(testClassChild2.add(1, 2)).toBe(600); // 两边的hook都会命中 - }); - }); - describe('extends class should also support createChild', () => { class NewInjector extends Injector { funcForNew() { diff --git a/test/injector/dispose.test.ts b/test/injector/dispose.test.ts index 8d094a5..a673c9c 100644 --- a/test/injector/dispose.test.ts +++ b/test/injector/dispose.test.ts @@ -135,9 +135,9 @@ describe('dispose', () => { expect(spy).toBeCalledTimes(1); injector.disposeOne(A); - const creatorA = injector.creatorMap.get(A); - expect(creatorA!.status).toBe(CreatorStatus.init); - expect(creatorA!.instance).toBeUndefined(); + const creatorA = injector.creatorMap.get(A)!; + expect(creatorA.status).toBe(CreatorStatus.init); + expect(creatorA.instance).toBeUndefined(); expect(instance.a).toBeInstanceOf(A); expect(spy).toBeCalledTimes(2);