From 4a9e4a345dfd1ece3f67ecca315fb0acba62025c Mon Sep 17 00:00:00 2001 From: zhangpanweb <37805064+zhangpanweb@users.noreply.github.com> Date: Sun, 3 Dec 2023 16:54:22 +0800 Subject: [PATCH 01/25] feat: make dispose support disposing instance of useFactory (#116) --- src/declare.ts | 3 +- src/decorator.ts | 3 +- src/helper/provider-helper.ts | 2 +- src/injector.ts | 86 +++++++++++++---------------- test/helper/is-function.test.ts | 2 +- test/helper/provider-helper.test.ts | 10 ++-- test/injector.test.ts | 7 --- test/injector/dispose.test.ts | 57 +++++++++++++++++++ test/injector/hasInstance.test.ts | 4 +- 9 files changed, 108 insertions(+), 66 deletions(-) diff --git a/src/declare.ts b/src/declare.ts index 2e33d2a..ac4c245 100644 --- a/src/declare.ts +++ b/src/declare.ts @@ -75,7 +75,7 @@ interface BasicCreator { /** * Store the instantiated object. */ - instance?: any; + instance?: Set; /** * Represent this creator is parsed from `Parameter`. and the params of Inject has set `default` attribution. */ @@ -83,7 +83,6 @@ interface BasicCreator { } export interface ValueCreator extends BasicCreator { - instance: any; status: CreatorStatus.done; } diff --git a/src/decorator.ts b/src/decorator.ts index 255e1ff..b1533e9 100644 --- a/src/decorator.ts +++ b/src/decorator.ts @@ -103,7 +103,8 @@ export function Autowired(token?: Token, opts?: InstanceOpts): PropertyDecorator } this[INSTANCE_KEY] = injector.get(realToken, opts); - injector.onceInstanceDisposed(this[INSTANCE_KEY], () => { + const [creator] = injector.getCreator(realToken); + injector.onceInstanceDisposed(creator!, () => { this[INSTANCE_KEY] = undefined; }); } diff --git a/src/helper/provider-helper.ts b/src/helper/provider-helper.ts index 72d5f29..fbf61df 100644 --- a/src/helper/provider-helper.ts +++ b/src/helper/provider-helper.ts @@ -45,7 +45,7 @@ export function parseCreatorFromProvider(provider: Provider): InstanceCreator { if (isValueProvider(provider)) { return { - instance: provider.useValue, + instance: new Set([provider.useValue]), isDefault: provider.isDefault, status: CreatorStatus.done, ...basicObj, diff --git a/src/injector.ts b/src/injector.ts index 595a208..b9c1bf0 100644 --- a/src/injector.ts +++ b/src/injector.ts @@ -19,6 +19,7 @@ import { Context, ParameterOpts, IDisposable, + FactoryCreator, } from './declare'; import { isClassCreator, @@ -35,7 +36,6 @@ import { EventEmitter } from './helper/event'; export class Injector { id = Helper.createId('Injector'); - private instanceIdGenerator = Helper.createIdFactory('Instance_' + this.id.slice(9)); depth = 0; tag?: string; @@ -45,9 +45,8 @@ export class Injector { private tagMatrix = new Map>(); private domainMap = new Map(); creatorMap = new Map(); - instanceRefMap = new Map(); - private instanceDisposedEmitter = new EventEmitter(); + private instanceDisposedEmitter = new EventEmitter(); constructor(providers: Provider[] = [], private opts: InjectorOpts = {}, parent?: Injector) { this.tag = opts.tag; @@ -191,7 +190,7 @@ export class Injector { */ hasInstance(instance: any) { for (const creator of this.creatorMap.values()) { - if (creator.instance === instance) { + if (creator.instance?.has(instance)) { return true; } } @@ -257,27 +256,33 @@ export class Injector { return this.hookStore.createOneHook(hook); } - onceInstanceDisposed(instance: any, cb: () => void) { - const instanceId = this.getInstanceId(instance); - if (!instanceId) { + onceInstanceDisposed(creator: InstanceCreator, cb: () => void) { + const instance = creator.instance; + if (!instance?.size) { return; } - return this.instanceDisposedEmitter.once(instanceId, cb); + return this.instanceDisposedEmitter.once(creator, cb); } disposeOne(token: Token, key = 'dispose') { const [creator] = this.getCreator(token); - if (!creator || creator.status === CreatorStatus.init) { + if (!creator) { return; } 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](); + let maybePromise: Promise | void; + if (instance) { + const disposeFns = Array.from(instance.values()) + .map((instanceItem) => { + if (typeof instanceItem[key] === 'function') { + return instanceItem[key](); + } + }) + .filter(Boolean); + + maybePromise = disposeFns.length ? Promise.all(disposeFns) : void 0; } creator.instance = undefined; @@ -285,18 +290,19 @@ export class Injector { if (maybePromise && Helper.isPromiseLike(maybePromise)) { maybePromise = maybePromise.then(() => { - instanceId && this.instanceDisposedEmitter.emit(instanceId); + instance && this.instanceDisposedEmitter.emit(creator); }); } else { - instanceId && this.instanceDisposedEmitter.emit(instanceId); + instance && this.instanceDisposedEmitter.emit(creator); } + return maybePromise; } disposeAll(key = 'dispose'): Promise { const creatorMap = this.creatorMap; - const promises: (Promise | undefined)[] = []; + const promises: (Promise | void)[] = []; for (const token of creatorMap.keys()) { promises.push(this.disposeOne(token, key)); @@ -355,7 +361,7 @@ export class Injector { const getInstance = () => { if (!instance) { instance = this.get(creator.useClass); - this.onceInstanceDisposed(instance, () => { + this.onceInstanceDisposed(creator, () => { if (toDispose) { toDispose.dispose(); toDispose = undefined; @@ -397,10 +403,6 @@ export class Injector { } } - private getNextInstanceId() { - return this.instanceIdGenerator.create(); - } - private resolveToken(token: Token): [Token, InstanceCreator | undefined] { let creator = this.creatorMap.get(token); @@ -419,7 +421,7 @@ export class Injector { return [token, creator]; } - private getCreator(token: Token): [InstanceCreator | null, Injector] { + getCreator(token: Token): [InstanceCreator | null, Injector] { const creator: InstanceCreator | undefined = this.resolveToken(token)[1]; if (creator) { @@ -433,14 +435,6 @@ 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; @@ -452,18 +446,18 @@ export class Injector { 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; + return creator.instance!.values().next().value; } return this.createInstanceFromClassCreator(ctx as Context, opts, args); } if (Helper.isFactoryCreator(creator)) { - return applyHooks(creator.useFactory(this), token, this.hookStore); + return this.createInstanceFromFactory(ctx as Context); } // must be ValueCreator, no need to hook. - return creator.instance; + return creator.instance!.values().next().value; } private createInstanceFromClassCreator(ctx: Context, opts: InstanceOpts, defaultArgs?: any[]) { @@ -481,16 +475,14 @@ export class Injector { try { const args = defaultArgs ?? this.getParameters(creator.parameters, ctx); - const nextId = this.getNextInstanceId(); - const instance = this.createInstanceWithInjector(cls, token, injector, args, nextId); - void this.getOrSaveInstanceId(instance, nextId); + const instance = this.createInstanceWithInjector(cls, token, injector, args); creator.status = CreatorStatus.init; // if not allow multiple, save the instance in creator. if (!opts.multiple) { creator.status = CreatorStatus.done; - creator.instance = instance; } + creator.instance ? creator.instance.add(instance) : (creator.instance = new Set([instance])); return instance; } catch (e) { @@ -524,13 +516,7 @@ export class Injector { }); } - private createInstanceWithInjector( - cls: ConstructorOf, - token: Token, - injector: Injector, - args: any[], - id: string, - ) { + private createInstanceWithInjector(cls: ConstructorOf, token: Token, injector: Injector, args: any[]) { // 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); @@ -540,14 +526,18 @@ export class 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: id, __injectorId: injector.id, }); return applyHooks(ret, token, this.hookStore); } - getInstanceId(instance: any) { - return this.instanceRefMap.get(instance); + private createInstanceFromFactory(ctx: Context) { + const { creator, token } = ctx; + + const value = creator.useFactory(this); + creator.instance = new Set([value]); + + return applyHooks(value, token, this.hookStore); } } diff --git a/test/helper/is-function.test.ts b/test/helper/is-function.test.ts index aade253..7bccba5 100644 --- a/test/helper/is-function.test.ts +++ b/test/helper/is-function.test.ts @@ -76,7 +76,7 @@ describe(__filename, () => { it('isValueCreator', () => { expect(Helper.isValueCreator(factoryProvider)).toBe(false); - expect(Helper.isValueCreator({ status: CreatorStatus.done, instance: A })).toBe(true); + expect(Helper.isValueCreator({ status: CreatorStatus.done, instance: new Set([A]) })).toBe(true); }); it('isFactoryCreator', () => { diff --git a/test/helper/provider-helper.test.ts b/test/helper/provider-helper.test.ts index 53f9022..37b8ff1 100644 --- a/test/helper/provider-helper.test.ts +++ b/test/helper/provider-helper.test.ts @@ -47,13 +47,15 @@ describe(__filename, () => { parameters: [], useClass: A, }); - expect(Helper.parseCreatorFromProvider(valueProvider)).toMatchObject({ - instance: expect.any(A), - status: CreatorStatus.done, - }); + + const creator = Helper.parseCreatorFromProvider(valueProvider); + expect(creator.instance?.has(A)).toBeTruthy; + expect(creator.status).toBe(CreatorStatus.done); + expect(Helper.parseCreatorFromProvider(factoryProvider)).toMatchObject({ useFactory: factoryProvider.useFactory, }); + expect(Helper.parseCreatorFromProvider(classProvider)).toMatchObject({ parameters: [], useClass: A, diff --git a/test/injector.test.ts b/test/injector.test.ts index 59f4645..4c05b18 100644 --- a/test/injector.test.ts +++ b/test/injector.test.ts @@ -428,13 +428,6 @@ describe('test injector work', () => { expect((instance1 as any).__injectorId).toBe(injector.id); expect((instance2 as any).__injectorId).toBe(injector.id); expect((instance3 as any).__injectorId).toBe(injector.id); - - expect((instance1 as any).__id.startsWith('Instance')).toBeTruthy(); - console.log(`file: injector.test.ts ~ it ~ (instance1 as any).__id:`, (instance1 as any).__id); - 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); }); }); diff --git a/test/injector/dispose.test.ts b/test/injector/dispose.test.ts index a673c9c..ed9ec01 100644 --- a/test/injector/dispose.test.ts +++ b/test/injector/dispose.test.ts @@ -250,6 +250,33 @@ describe('dispose asynchronous', () => { expect(spy).toBeCalledTimes(1); }); + it("dispose creator with multiple instance will call call instances's dispose method", async () => { + const spy = jest.fn(); + + @Injectable({ multiple: true }) + class DisposeCls { + dispose = async () => { + spy(); + }; + } + + const instance = injector.get(DisposeCls); + expect(injector.hasInstance(instance)).toBeTruthy(); + expect(instance).toBeInstanceOf(DisposeCls); + + const instance2 = injector.get(DisposeCls); + expect(injector.hasInstance(instance2)).toBeTruthy(); + expect(instance2).toBeInstanceOf(DisposeCls); + + await injector.disposeOne(DisposeCls); + expect(injector.hasInstance(instance)).toBeFalsy(); + expect(injector.hasInstance(instance2)).toBeFalsy(); + expect(spy).toBeCalledTimes(2); + + await injector.disposeOne(DisposeCls); + expect(spy).toBeCalledTimes(2); + }); + it("dispose an instance will also dispose it's instance", async () => { const spy = jest.fn(); @@ -283,4 +310,34 @@ describe('dispose asynchronous', () => { expect(instance.a).toBeInstanceOf(A); expect(spy).toBeCalledTimes(2); }); + + it('dispose should dispose instance of useFactory', () => { + const injector = new Injector(); + let aValue = 1; + const token = Symbol.for('A'); + + injector.addProviders( + ...[ + { + token, + useFactory: () => aValue, + }, + ], + ); + + @Injectable() + class B { + @Autowired(token) + a!: number; + } + + const instance = injector.get(B); + expect(injector.hasInstance(instance)).toBeTruthy(); + expect(instance).toBeInstanceOf(B); + expect(instance.a).toBe(1); + + injector.disposeOne(token); + aValue = 2; + expect(instance.a).toBe(2); + }); }); diff --git a/test/injector/hasInstance.test.ts b/test/injector/hasInstance.test.ts index 2607387..5729721 100644 --- a/test/injector/hasInstance.test.ts +++ b/test/injector/hasInstance.test.ts @@ -25,7 +25,7 @@ describe('hasInstance', () => { expect(injector.hasInstance(b)).toBe(true); const c = injector.get(C); - expect(injector.hasInstance(c)).toBe(false); + expect(injector.hasInstance(c)).toBe(true); }); it('hasInstance 支持 primitive 的判断', () => { @@ -40,6 +40,6 @@ describe('hasInstance', () => { expect(injector.hasInstance(b)).toBe(true); const c = injector.get(C); - expect(injector.hasInstance(c)).toBe(false); + expect(injector.hasInstance(c)).toBe(true); }); }); From dad48a81f47033e090202667cd4b3fccb9fa8e14 Mon Sep 17 00:00:00 2001 From: artin Date: Sun, 3 Dec 2023 17:00:03 +0800 Subject: [PATCH 02/25] fix: factory instances should save proxied --- src/injector.ts | 9 +++------ test/injector.test.ts | 7 ++++--- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/injector.ts b/src/injector.ts index b9c1bf0..b56de4e 100644 --- a/src/injector.ts +++ b/src/injector.ts @@ -272,7 +272,7 @@ export class Injector { const instance = creator.instance; - let maybePromise: Promise | void; + let maybePromise: Promise | void | undefined; if (instance) { const disposeFns = Array.from(instance.values()) .map((instanceItem) => { @@ -525,9 +525,6 @@ export class Injector { // mount injector on the instance, so that the inner object can access the injector in the future. setInjector(ret, injector); - Object.assign(ret, { - __injectorId: injector.id, - }); return applyHooks(ret, token, this.hookStore); } @@ -535,9 +532,9 @@ export class Injector { private createInstanceFromFactory(ctx: Context) { const { creator, token } = ctx; - const value = creator.useFactory(this); + const value = applyHooks(creator.useFactory(this), token, this.hookStore); creator.instance = new Set([value]); - return applyHooks(value, token, this.hookStore); + return value; } } diff --git a/test/injector.test.ts b/test/injector.test.ts index 4c05b18..c868f8d 100644 --- a/test/injector.test.ts +++ b/test/injector.test.ts @@ -19,6 +19,7 @@ import { IAfterThrowingJoinPoint, } from '../src'; import * as InjectorError from '../src/error'; +import { getInjectorOfInstance } from '../src/helper'; describe('test injector work', () => { @Injectable() @@ -425,9 +426,9 @@ describe('test injector work', () => { const instance2 = injector.get(A); const instance3 = injector.get(B); - expect((instance1 as any).__injectorId).toBe(injector.id); - expect((instance2 as any).__injectorId).toBe(injector.id); - expect((instance3 as any).__injectorId).toBe(injector.id); + expect(getInjectorOfInstance(instance1)!.id).toBe(injector.id); + expect(getInjectorOfInstance(instance2)!.id).toBe(injector.id); + expect(getInjectorOfInstance(instance3)!.id).toBe(injector.id); }); }); From 69443de55ace59d88153498ccf3a2428b16877a3 Mon Sep 17 00:00:00 2001 From: artin Date: Sun, 3 Dec 2023 17:01:33 +0800 Subject: [PATCH 03/25] style: use eslint no-void --- .eslintrc.js | 6 ++++++ src/injector.ts | 4 ++-- test/helper/hook-helper.test.ts | 16 ++++++++-------- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index a506cf4..707768a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -24,5 +24,11 @@ module.exports = { allowedNames: ['self', 'injector'], }, ], + 'no-void': [ + 'error', + { + allowAsStatement: true, + }, + ], }, }; diff --git a/src/injector.ts b/src/injector.ts index b56de4e..95f2565 100644 --- a/src/injector.ts +++ b/src/injector.ts @@ -282,7 +282,7 @@ export class Injector { }) .filter(Boolean); - maybePromise = disposeFns.length ? Promise.all(disposeFns) : void 0; + maybePromise = disposeFns.length ? Promise.all(disposeFns) : undefined; } creator.instance = undefined; @@ -436,7 +436,7 @@ export class Injector { } private createInstance(ctx: Context, defaultOpts?: InstanceOpts, args?: any[]) { - const { creator, token } = ctx; + const { creator } = ctx; if (creator.dropdownForTag && creator.tag !== this.tag) { throw InjectorError.tagOnlyError(String(creator.tag), String(this.tag)); diff --git a/test/helper/hook-helper.test.ts b/test/helper/hook-helper.test.ts index f1ca8a1..2a81a3c 100644 --- a/test/helper/hook-helper.test.ts +++ b/test/helper/hook-helper.test.ts @@ -2,7 +2,6 @@ import { HookStore, applyHooks, isHooked } from '../../src/helper'; import { HookType } from '../../src'; describe('hook store test', () => { - const token1 = Symbol(); it('可以创建和删除hook', () => { @@ -59,17 +58,18 @@ describe('hook store test', () => { expect(valueRet).toBe('1'); const hookStore = new HookStore(); - hookStore.createHooks([{ - hook: () => void 0, - method: 'a', - target: token1, - type: HookType.After, - }]); + hookStore.createHooks([ + { + hook: () => undefined, + method: 'a', + target: token1, + type: HookType.After, + }, + ]); const objectRet = applyHooks({}, token1, hookStore); expect(isHooked(objectRet)).toBeTruthy(); const objectRetNoHooks = applyHooks({}, token1, new HookStore()); expect(isHooked(objectRetNoHooks)).toBeFalsy(); - }); }); From edaa25e85602374642e55948415e3a656035e06a Mon Sep 17 00:00:00 2001 From: artin Date: Sun, 3 Dec 2023 17:07:31 +0800 Subject: [PATCH 04/25] fix: factory support multiple value --- src/injector.ts | 3 +-- test/injector.test.ts | 23 ++--------------------- 2 files changed, 3 insertions(+), 23 deletions(-) diff --git a/src/injector.ts b/src/injector.ts index 95f2565..0da2493 100644 --- a/src/injector.ts +++ b/src/injector.ts @@ -533,8 +533,7 @@ export class Injector { const { creator, token } = ctx; const value = applyHooks(creator.useFactory(this), token, this.hookStore); - creator.instance = new Set([value]); - + creator.instance ? creator.instance.add(value) : (creator.instance = new Set([value])); return value; } } diff --git a/test/injector.test.ts b/test/injector.test.ts index c868f8d..65f7e46 100644 --- a/test/injector.test.ts +++ b/test/injector.test.ts @@ -1,23 +1,4 @@ -import { - Autowired, - Injectable, - Injector, - INJECTOR_TOKEN, - Inject, - Optional, - Aspect, - Before, - After, - Around, - HookType, - IBeforeJoinPoint, - IAfterJoinPoint, - IAroundJoinPoint, - AfterReturning, - IAfterReturningJoinPoint, - AfterThrowing, - IAfterThrowingJoinPoint, -} from '../src'; +import { Autowired, Injectable, Injector, INJECTOR_TOKEN, Inject, Optional } from '../src'; import * as InjectorError from '../src/error'; import { getInjectorOfInstance } from '../src/helper'; @@ -420,7 +401,7 @@ describe('test injector work', () => { expect(injector1.id).not.toBe(injector2.id); }); - it('实例对象有 ID 数据', () => { + it('could get id from the instance', () => { const injector = new Injector(); const instance1 = injector.get(A); const instance2 = injector.get(A); From 362196f50bb4f8a476b0d8c108dfbdb1455e1d9a Mon Sep 17 00:00:00 2001 From: artin Date: Sun, 3 Dec 2023 17:34:40 +0800 Subject: [PATCH 05/25] fix: we should listen on instance disposed --- src/decorator.ts | 3 +-- src/injector.ts | 34 +++++++++++++++++----------------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/decorator.ts b/src/decorator.ts index b1533e9..255e1ff 100644 --- a/src/decorator.ts +++ b/src/decorator.ts @@ -103,8 +103,7 @@ export function Autowired(token?: Token, opts?: InstanceOpts): PropertyDecorator } this[INSTANCE_KEY] = injector.get(realToken, opts); - const [creator] = injector.getCreator(realToken); - injector.onceInstanceDisposed(creator!, () => { + injector.onceInstanceDisposed(this[INSTANCE_KEY], () => { this[INSTANCE_KEY] = undefined; }); } diff --git a/src/injector.ts b/src/injector.ts index 0da2493..7c33731 100644 --- a/src/injector.ts +++ b/src/injector.ts @@ -46,7 +46,7 @@ export class Injector { private domainMap = new Map(); creatorMap = new Map(); - private instanceDisposedEmitter = new EventEmitter(); + private instanceDisposedEmitter = new EventEmitter(); constructor(providers: Provider[] = [], private opts: InjectorOpts = {}, parent?: Injector) { this.tag = opts.tag; @@ -256,12 +256,8 @@ export class Injector { return this.hookStore.createOneHook(hook); } - onceInstanceDisposed(creator: InstanceCreator, cb: () => void) { - const instance = creator.instance; - if (!instance?.size) { - return; - } - return this.instanceDisposedEmitter.once(creator, cb); + onceInstanceDisposed(instance: any, cb: () => void) { + return this.instanceDisposedEmitter.once(instance, cb); } disposeOne(token: Token, key = 'dispose') { @@ -270,17 +266,21 @@ export class Injector { return; } + if (creator.status === CreatorStatus.init) { + return; + } + const instance = creator.instance; let maybePromise: Promise | void | undefined; if (instance) { - const disposeFns = Array.from(instance.values()) - .map((instanceItem) => { - if (typeof instanceItem[key] === 'function') { - return instanceItem[key](); - } - }) - .filter(Boolean); + const disposeFns = [] as any[]; + + for (const i of instance.values()) { + if (i && typeof i[key] === 'function') { + disposeFns.push(i[key]()); + } + } maybePromise = disposeFns.length ? Promise.all(disposeFns) : undefined; } @@ -290,10 +290,10 @@ export class Injector { if (maybePromise && Helper.isPromiseLike(maybePromise)) { maybePromise = maybePromise.then(() => { - instance && this.instanceDisposedEmitter.emit(creator); + instance && this.instanceDisposedEmitter.emit(instance); }); } else { - instance && this.instanceDisposedEmitter.emit(creator); + instance && this.instanceDisposedEmitter.emit(instance); } return maybePromise; @@ -361,7 +361,7 @@ export class Injector { const getInstance = () => { if (!instance) { instance = this.get(creator.useClass); - this.onceInstanceDisposed(creator, () => { + this.onceInstanceDisposed(instance, () => { if (toDispose) { toDispose.dispose(); toDispose = undefined; From 5f09c6a17dd79175c14a6a688b06892572699024 Mon Sep 17 00:00:00 2001 From: artin Date: Sun, 3 Dec 2023 20:03:52 +0800 Subject: [PATCH 06/25] fix: instance should be disposed --- src/injector.ts | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/injector.ts b/src/injector.ts index 7c33731..7f1a4e2 100644 --- a/src/injector.ts +++ b/src/injector.ts @@ -276,10 +276,21 @@ export class Injector { if (instance) { const disposeFns = [] as any[]; - for (const i of instance.values()) { - if (i && typeof i[key] === 'function') { - disposeFns.push(i[key]()); + for (const item of instance.values()) { + let _maybePromise: Promise | undefined; + if (item && typeof item[key] === 'function') { + _maybePromise = item[key](); + + if (_maybePromise && Helper.isPromiseLike(_maybePromise)) { + _maybePromise = _maybePromise.then(() => { + this.instanceDisposedEmitter.emit(item); + }); + } else { + this.instanceDisposedEmitter.emit(item); + } } + + _maybePromise && disposeFns.push(_maybePromise); } maybePromise = disposeFns.length ? Promise.all(disposeFns) : undefined; @@ -288,14 +299,6 @@ export class Injector { creator.instance = undefined; creator.status = CreatorStatus.init; - if (maybePromise && Helper.isPromiseLike(maybePromise)) { - maybePromise = maybePromise.then(() => { - instance && this.instanceDisposedEmitter.emit(instance); - }); - } else { - instance && this.instanceDisposedEmitter.emit(instance); - } - return maybePromise; } From cf82705a8f1a247545f7ade537f01094cb0c94be Mon Sep 17 00:00:00 2001 From: artin Date: Sun, 3 Dec 2023 20:17:53 +0800 Subject: [PATCH 07/25] refactor: use instances to store objects --- src/declare.ts | 4 +-- src/helper/provider-helper.ts | 2 +- src/injector.ts | 38 +++++++++++++---------------- test/helper/is-function.test.ts | 2 +- test/helper/provider-helper.test.ts | 2 +- test/injector/dispose.test.ts | 16 ++++++------ 6 files changed, 30 insertions(+), 34 deletions(-) diff --git a/src/declare.ts b/src/declare.ts index ac4c245..6f45220 100644 --- a/src/declare.ts +++ b/src/declare.ts @@ -73,9 +73,9 @@ interface BasicCreator { dropdownForTag?: boolean; status?: CreatorStatus; /** - * Store the instantiated object. + * Store the instantiated objects. */ - instance?: Set; + instances?: Set; /** * Represent this creator is parsed from `Parameter`. and the params of Inject has set `default` attribution. */ diff --git a/src/helper/provider-helper.ts b/src/helper/provider-helper.ts index fbf61df..81da592 100644 --- a/src/helper/provider-helper.ts +++ b/src/helper/provider-helper.ts @@ -45,7 +45,7 @@ export function parseCreatorFromProvider(provider: Provider): InstanceCreator { if (isValueProvider(provider)) { return { - instance: new Set([provider.useValue]), + instances: new Set([provider.useValue]), isDefault: provider.isDefault, status: CreatorStatus.done, ...basicObj, diff --git a/src/injector.ts b/src/injector.ts index 7f1a4e2..70f8fba 100644 --- a/src/injector.ts +++ b/src/injector.ts @@ -190,7 +190,7 @@ export class Injector { */ hasInstance(instance: any) { for (const creator of this.creatorMap.values()) { - if (creator.instance?.has(instance)) { + if (creator.instances?.has(instance)) { return true; } } @@ -266,37 +266,33 @@ export class Injector { return; } - if (creator.status === CreatorStatus.init) { - return; - } - - const instance = creator.instance; + const instances = creator.instances; let maybePromise: Promise | void | undefined; - if (instance) { + if (instances) { const disposeFns = [] as any[]; - for (const item of instance.values()) { + for (const item of instances.values()) { let _maybePromise: Promise | undefined; if (item && typeof item[key] === 'function') { _maybePromise = item[key](); + } - if (_maybePromise && Helper.isPromiseLike(_maybePromise)) { - _maybePromise = _maybePromise.then(() => { + if (_maybePromise && Helper.isPromiseLike(_maybePromise)) { + disposeFns.push( + _maybePromise.then(() => { this.instanceDisposedEmitter.emit(item); - }); - } else { - this.instanceDisposedEmitter.emit(item); - } + }), + ); + } else { + this.instanceDisposedEmitter.emit(item); } - - _maybePromise && disposeFns.push(_maybePromise); } maybePromise = disposeFns.length ? Promise.all(disposeFns) : undefined; } - creator.instance = undefined; + creator.instances = undefined; creator.status = CreatorStatus.init; return maybePromise; @@ -449,7 +445,7 @@ export class Injector { 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!.values().next().value; + return creator.instances!.values().next().value; } return this.createInstanceFromClassCreator(ctx as Context, opts, args); @@ -460,7 +456,7 @@ export class Injector { } // must be ValueCreator, no need to hook. - return creator.instance!.values().next().value; + return creator.instances!.values().next().value; } private createInstanceFromClassCreator(ctx: Context, opts: InstanceOpts, defaultArgs?: any[]) { @@ -485,7 +481,7 @@ export class Injector { if (!opts.multiple) { creator.status = CreatorStatus.done; } - creator.instance ? creator.instance.add(instance) : (creator.instance = new Set([instance])); + creator.instances ? creator.instances.add(instance) : (creator.instances = new Set([instance])); return instance; } catch (e) { @@ -536,7 +532,7 @@ export class Injector { const { creator, token } = ctx; const value = applyHooks(creator.useFactory(this), token, this.hookStore); - creator.instance ? creator.instance.add(value) : (creator.instance = new Set([value])); + creator.instances ? creator.instances.add(value) : (creator.instances = new Set([value])); return value; } } diff --git a/test/helper/is-function.test.ts b/test/helper/is-function.test.ts index 7bccba5..7dfa78d 100644 --- a/test/helper/is-function.test.ts +++ b/test/helper/is-function.test.ts @@ -76,7 +76,7 @@ describe(__filename, () => { it('isValueCreator', () => { expect(Helper.isValueCreator(factoryProvider)).toBe(false); - expect(Helper.isValueCreator({ status: CreatorStatus.done, instance: new Set([A]) })).toBe(true); + expect(Helper.isValueCreator({ status: CreatorStatus.done, instances: new Set([A]) })).toBe(true); }); it('isFactoryCreator', () => { diff --git a/test/helper/provider-helper.test.ts b/test/helper/provider-helper.test.ts index 37b8ff1..2269083 100644 --- a/test/helper/provider-helper.test.ts +++ b/test/helper/provider-helper.test.ts @@ -49,7 +49,7 @@ describe(__filename, () => { }); const creator = Helper.parseCreatorFromProvider(valueProvider); - expect(creator.instance?.has(A)).toBeTruthy; + expect(creator.instances?.has(A)).toBeTruthy; expect(creator.status).toBe(CreatorStatus.done); expect(Helper.parseCreatorFromProvider(factoryProvider)).toMatchObject({ diff --git a/test/injector/dispose.test.ts b/test/injector/dispose.test.ts index ed9ec01..33796d6 100644 --- a/test/injector/dispose.test.ts +++ b/test/injector/dispose.test.ts @@ -44,7 +44,7 @@ describe('dispose', () => { const creator = injector.creatorMap.get(A); expect(creator!.status).toBe(CreatorStatus.init); - expect(creator!.instance).toBeUndefined(); + expect(creator!.instances).toBeUndefined(); const a2 = injector.get(A); expect(a).not.toBe(a2); @@ -62,11 +62,11 @@ describe('dispose', () => { const creatorA = injector.creatorMap.get(A); expect(creatorA!.status).toBe(CreatorStatus.init); - expect(creatorA!.instance).toBeUndefined(); + expect(creatorA!.instances).toBeUndefined(); const creatorB = injector.creatorMap.get(B); expect(creatorB!.status).toBe(CreatorStatus.init); - expect(creatorB!.instance).toBeUndefined(); + expect(creatorB!.instances).toBeUndefined(); const a2 = injector.get(A); expect(a).not.toBe(a2); @@ -137,7 +137,7 @@ describe('dispose', () => { injector.disposeOne(A); const creatorA = injector.creatorMap.get(A)!; expect(creatorA.status).toBe(CreatorStatus.init); - expect(creatorA.instance).toBeUndefined(); + expect(creatorA.instances).toBeUndefined(); expect(instance.a).toBeInstanceOf(A); expect(spy).toBeCalledTimes(2); @@ -181,7 +181,7 @@ describe('dispose asynchronous', () => { const creator = injector.creatorMap.get(A); expect(creator!.status).toBe(CreatorStatus.init); - expect(creator!.instance).toBeUndefined(); + expect(creator!.instances).toBeUndefined(); const a2 = injector.get(A); expect(a).not.toBe(a2); @@ -199,11 +199,11 @@ describe('dispose asynchronous', () => { const creatorA = injector.creatorMap.get(A); expect(creatorA!.status).toBe(CreatorStatus.init); - expect(creatorA!.instance).toBeUndefined(); + expect(creatorA!.instances).toBeUndefined(); const creatorB = injector.creatorMap.get(B); expect(creatorB!.status).toBe(CreatorStatus.init); - expect(creatorB!.instance).toBeUndefined(); + expect(creatorB!.instances).toBeUndefined(); const a2 = injector.get(A); expect(a).not.toBe(a2); @@ -305,7 +305,7 @@ describe('dispose asynchronous', () => { await injector.disposeOne(A); const creatorA = injector.creatorMap.get(A); expect(creatorA!.status).toBe(CreatorStatus.init); - expect(creatorA!.instance).toBeUndefined(); + expect(creatorA!.instances).toBeUndefined(); expect(instance.a).toBeInstanceOf(A); expect(spy).toBeCalledTimes(2); From 1e6f2001e1e6fd2551a885a93dc5a059e183d161 Mon Sep 17 00:00:00 2001 From: artin Date: Sun, 3 Dec 2023 20:29:46 +0800 Subject: [PATCH 08/25] chore: remove unused code --- src/helper/injector-helper.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/helper/injector-helper.ts b/src/helper/injector-helper.ts index 99aa5d6..6a6eaf0 100644 --- a/src/helper/injector-helper.ts +++ b/src/helper/injector-helper.ts @@ -39,12 +39,3 @@ let index = 0; export function createId(name: string) { return `${name}_${index++}`; } - -export function createIdFactory(name: string) { - let idx = 0; - return { - create() { - return `${name}_${idx++}`; - }, - }; -} From c5e1a53f95a2c88ed03fdbfaf3bcafc286751c77 Mon Sep 17 00:00:00 2001 From: artin Date: Sun, 3 Dec 2023 20:40:32 +0800 Subject: [PATCH 09/25] test: update testcase --- test/injector/dispose.test.ts | 2 +- test/injector/hasInstance.test.ts | 20 ++++++-------------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/test/injector/dispose.test.ts b/test/injector/dispose.test.ts index 33796d6..dd60b95 100644 --- a/test/injector/dispose.test.ts +++ b/test/injector/dispose.test.ts @@ -250,7 +250,7 @@ describe('dispose asynchronous', () => { expect(spy).toBeCalledTimes(1); }); - it("dispose creator with multiple instance will call call instances's dispose method", async () => { + it("dispose creator with multiple instance will call instances's dispose method", async () => { const spy = jest.fn(); @Injectable({ multiple: true }) diff --git a/test/injector/hasInstance.test.ts b/test/injector/hasInstance.test.ts index 5729721..cb4e9e7 100644 --- a/test/injector/hasInstance.test.ts +++ b/test/injector/hasInstance.test.ts @@ -16,26 +16,18 @@ describe('hasInstance', () => { it('能够通过 hasInstance 查到单例对象的存在性', () => { const token = 'token'; const instance = {}; - const provider = { token, useValue: instance }; - const injector = new Injector([provider, B, C]); - - expect(injector.hasInstance(instance)).toBe(true); - const b = injector.get(B); - expect(injector.hasInstance(b)).toBe(true); + const token2 = 'token2'; + const instance2 = true; - const c = injector.get(C); - expect(injector.hasInstance(c)).toBe(true); - }); - - it('hasInstance 支持 primitive 的判断', () => { - const token = 'token'; - const instance = true; const provider = { token, useValue: instance }; - const injector = new Injector([provider, B, C]); + const injector = new Injector([provider, B, C, { token: token2, useValue: instance2 }]); expect(injector.hasInstance(instance)).toBe(true); + // 支持 primitive 的判断 + expect(injector.hasInstance(instance2)).toBe(true); + const b = injector.get(B); expect(injector.hasInstance(b)).toBe(true); From be0c3294253e36cfe3e6b37d8ec82d646977d1b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=87=8E=E5=A3=B0?= Date: Mon, 4 Dec 2023 10:32:09 +0800 Subject: [PATCH 10/25] refactor: remove import all statements --- src/decorator.ts | 39 ++++++++++++++---------- src/helper/injector-helper.ts | 12 ++++++-- src/helper/provider-helper.ts | 4 +-- src/injector.ts | 56 +++++++++++++++++++++-------------- 4 files changed, 69 insertions(+), 42 deletions(-) diff --git a/src/decorator.ts b/src/decorator.ts index 255e1ff..57dafe8 100644 --- a/src/decorator.ts +++ b/src/decorator.ts @@ -1,6 +1,4 @@ import 'reflect-metadata'; -import * as Helper from './helper'; -import * as Error from './error'; import { Token, InstanceOpts, @@ -14,7 +12,18 @@ import { IAfterThrowingAspectHookFunction, IAroundHookOptions, } from './declare'; -import { markAsAspect, markAsHook } from './helper'; +import { + addDeps, + getInjectorOfInstance, + getParameterDeps, + isToken, + markAsAspect, + markAsHook, + markInjectable, + setParameterIn, + setParameters, +} from './helper'; +import { noInjectorError, notInjectError, tokenInvalidError } from './error'; /** * 装饰一个 Class 是否是可以被依赖注入 @@ -22,11 +31,11 @@ import { markAsAspect, markAsHook } from './helper'; */ export function Injectable(opts?: InstanceOpts): ClassDecorator { return (target: T) => { - Helper.markInjectable(target, opts); + markInjectable(target, opts); const params = Reflect.getMetadata('design:paramtypes', target); if (Array.isArray(params)) { - Helper.setParameters(target, params); + setParameters(target, params); // 如果支持多例创建,就不检查构造函数依赖的可注入性 if (opts && opts.multiple) { @@ -34,10 +43,10 @@ export function Injectable(opts?: InstanceOpts): ClassDecorator { } // 检查依赖的可注入性 - const depTokens = Helper.getParameterDeps(target); + const depTokens = getParameterDeps(target); depTokens.forEach((item, index) => { - if (!Helper.isToken(item)) { - throw Error.notInjectError(target, index); + if (!isToken(item)) { + throw notInjectError(target, index); } }); } @@ -57,7 +66,7 @@ interface InjectOpts { */ export function Inject(token: Token, opts: InjectOpts = {}): ParameterDecorator { return (target, _: string | symbol | undefined, index: number) => { - Helper.setParameterIn(target, { ...opts, token }, index); + setParameterIn(target, { ...opts, token }, index); }; } @@ -67,7 +76,7 @@ export function Inject(token: Token, opts: InjectOpts = {}): ParameterDecorator */ export function Optional(token: Token = Symbol()): ParameterDecorator { return (target, _: string | symbol | undefined, index: number) => { - Helper.setParameterIn(target, { default: undefined, token }, index); + setParameterIn(target, { default: undefined, token }, index); }; } @@ -84,22 +93,22 @@ export function Autowired(token?: Token, opts?: InstanceOpts): PropertyDecorator realToken = Reflect.getMetadata('design:type', target, propertyKey); } - if (!Helper.isToken(realToken)) { - throw Error.tokenInvalidError(target, propertyKey, realToken); + if (!isToken(realToken)) { + throw tokenInvalidError(target, propertyKey, realToken); } // 添加构造函数的依赖 - Helper.addDeps(target, realToken); + addDeps(target, realToken); const descriptor: PropertyDescriptor = { configurable: true, enumerable: true, get(this: any) { if (!this[INSTANCE_KEY]) { - const injector = Helper.getInjectorOfInstance(this); + const injector = getInjectorOfInstance(this); if (!injector) { - throw Error.noInjectorError(this); + throw noInjectorError(this); } this[INSTANCE_KEY] = injector.get(realToken, opts); diff --git a/src/helper/injector-helper.ts b/src/helper/injector-helper.ts index 6a6eaf0..370a2f9 100644 --- a/src/helper/injector-helper.ts +++ b/src/helper/injector-helper.ts @@ -35,7 +35,13 @@ export function isInjectable(target: object) { return !!getInjectableOpts(target); } -let index = 0; -export function createId(name: string) { - return `${name}_${index++}`; +export function createIdFactory(name: string) { + let idx = 0; + return { + next() { + return `${name}_${idx++}`; + }, + }; } + +export const injectorIdGenerator = createIdFactory('Injector'); diff --git a/src/helper/provider-helper.ts b/src/helper/provider-helper.ts index 81da592..9305f57 100644 --- a/src/helper/provider-helper.ts +++ b/src/helper/provider-helper.ts @@ -1,4 +1,3 @@ -import * as Error from '../error'; import { Provider, Token, Tag, InstanceCreator, CreatorStatus, InstanceOpts } from '../declare'; import { isValueProvider, @@ -11,6 +10,7 @@ import { import { getParameterOpts } from './parameter-helper'; import { getAllDeps } from './dep-helper'; import { getInjectableOpts } from './injector-helper'; +import { noInjectableError } from '../error'; export function getProvidersFromTokens(targets: Token[]) { const spreadDeps: Token[] = getAllDeps(...targets); @@ -66,7 +66,7 @@ export function parseCreatorFromProvider(provider: Provider): InstanceCreator { const opts = getInjectableOpts(useClass); if (!opts) { - throw Error.noInjectableError(useClass); + throw noInjectableError(useClass); } const parameters = getParameterOpts(useClass); diff --git a/src/injector.ts b/src/injector.ts index 70f8fba..41b7855 100644 --- a/src/injector.ts +++ b/src/injector.ts @@ -1,5 +1,17 @@ -import * as Helper from './helper'; -import * as InjectorError from './error'; +import { + flatten, + getAllDeps, + getParameterOpts, + hasTag, + injectorIdGenerator, + isFactoryCreator, + isInjectableToken, + isPromiseLike, + parseCreatorFromProvider, + parseTokenFromProvider, + uniq, +} from './helper'; +import { aliasCircularError, circularError, noProviderError, tagOnlyError } from './error'; import { INJECTOR_TOKEN, Provider, @@ -35,7 +47,7 @@ import { import { EventEmitter } from './helper/event'; export class Injector { - id = Helper.createId('Injector'); + id = injectorIdGenerator.next(); depth = 0; tag?: string; @@ -130,12 +142,12 @@ export class Injector { useClass: token, }; } else { - throw InjectorError.noProviderError(token); + throw noProviderError(token); } } } else { // firstly, use tag to exchange token - if (opts && Helper.hasTag(opts)) { + if (opts && hasTag(opts)) { const tagToken = this.exchangeToken(token, opts.tag); [creator, injector] = this.getCreator(tagToken); } @@ -153,7 +165,7 @@ export class Injector { } if (!creator) { - throw InjectorError.noProviderError(token); + throw noProviderError(token); } const ctx = { @@ -207,14 +219,14 @@ export class Injector { } parseDependencies(...targets: Token[]) { - const deepDeps: Token[] = Helper.getAllDeps(...targets); + const deepDeps: Token[] = getAllDeps(...targets); const allDeps = targets.concat(deepDeps); - const providers = Helper.uniq(allDeps.filter(Helper.isInjectableToken)); + const providers = uniq(allDeps.filter(isInjectableToken)); this.setProviders(providers, { deep: true }); - const defaultProviders = Helper.flatten( + const defaultProviders = flatten( providers.map((p) => { - return Helper.getParameterOpts(p); + return getParameterOpts(p); }), ) .filter((opt) => { @@ -231,7 +243,7 @@ export class Injector { // make sure all dependencies have corresponding providers const notProvidedDeps = allDeps.filter((d) => !this.getCreator(d)[0]); if (notProvidedDeps.length) { - throw InjectorError.noProviderError(...notProvidedDeps); + throw noProviderError(...notProvidedDeps); } } @@ -278,7 +290,7 @@ export class Injector { _maybePromise = item[key](); } - if (_maybePromise && Helper.isPromiseLike(_maybePromise)) { + if (_maybePromise && isPromiseLike(_maybePromise)) { disposeFns.push( _maybePromise.then(() => { this.instanceDisposedEmitter.emit(item); @@ -326,13 +338,13 @@ export class Injector { private setProviders(providers: Provider[], opts: AddProvidersOpts = {}) { for (const provider of providers) { - const originToken = Helper.parseTokenFromProvider(provider); - const token = Helper.hasTag(provider) ? this.exchangeToken(originToken, provider.tag) : originToken; + const originToken = parseTokenFromProvider(provider); + const token = hasTag(provider) ? this.exchangeToken(originToken, provider.tag) : originToken; const current = opts.deep ? this.getCreator(token)[0] : this.resolveToken(token)[1]; const shouldBeSet = [ // use provider's override attribute. - Helper.isTypeProvider(provider) ? false : provider.override, + isTypeProvider(provider) ? false : provider.override, // use opts.override. The user explicitly call `overrideProviders`. opts.override, // if this token do not have corresponding creator, use override @@ -341,7 +353,7 @@ export class Injector { ].some(Boolean); if (shouldBeSet) { - const creator = Helper.parseCreatorFromProvider(provider); + const creator = parseCreatorFromProvider(provider); this.creatorMap.set(token, creator); // use effect to Make sure there are no cycles @@ -411,7 +423,7 @@ export class Injector { token = creator.useAlias; if (paths.includes(token)) { - throw InjectorError.aliasCircularError(paths, token); + throw aliasCircularError(paths, token); } paths.push(token); creator = this.creatorMap.get(token); @@ -438,10 +450,10 @@ export class Injector { const { creator } = ctx; if (creator.dropdownForTag && creator.tag !== this.tag) { - throw InjectorError.tagOnlyError(String(creator.tag), String(this.tag)); + throw tagOnlyError(String(creator.tag), String(this.tag)); } - if (Helper.isClassCreator(creator)) { + if (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) { @@ -451,7 +463,7 @@ export class Injector { return this.createInstanceFromClassCreator(ctx as Context, opts, args); } - if (Helper.isFactoryCreator(creator)) { + if (isFactoryCreator(creator)) { return this.createInstanceFromFactory(ctx as Context); } @@ -467,7 +479,7 @@ export class Injector { // If you try to create an instance whose status is creating, it must be caused by circular dependencies. if (currentStatus === CreatorStatus.creating) { - throw InjectorError.circularError(cls, ctx); + throw circularError(cls, ctx); } creator.status = CreatorStatus.creating; @@ -511,7 +523,7 @@ export class Injector { if (!creator && Object.prototype.hasOwnProperty.call(opts, 'default')) { return opts.default; } - throw InjectorError.noProviderError(opts.token); + throw noProviderError(opts.token); }); } From 82dac47fb09b770c4d25a9d162cf75d882cd8334 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=87=8E=E5=A3=B0?= Date: Mon, 4 Dec 2023 10:46:27 +0800 Subject: [PATCH 11/25] refactor: update folder structure --- src/{helper => }/compose.ts | 2 +- src/decorator.ts | 8 ++++---- src/error.ts | 2 +- src/factoryHelper.ts | 2 +- src/helper/dep-helper.ts | 4 ++-- src/helper/hook-helper.ts | 4 ++-- src/helper/index.ts | 2 +- src/helper/injector-helper.ts | 2 +- src/helper/is-function.ts | 2 +- src/helper/parameter-helper.ts | 2 +- src/helper/provider-helper.ts | 2 +- src/helper/{util.ts => utils.ts} | 0 src/index.ts | 2 +- src/injector.ts | 2 +- src/{declare.ts => types.ts} | 0 test/helper/compose.test.ts | 2 +- test/helper/util-helper.test.ts | 2 +- 17 files changed, 20 insertions(+), 20 deletions(-) rename src/{helper => }/compose.ts (95%) rename src/helper/{util.ts => utils.ts} (100%) rename src/{declare.ts => types.ts} (100%) diff --git a/src/helper/compose.ts b/src/compose.ts similarity index 95% rename from src/helper/compose.ts rename to src/compose.ts index ddaf1b4..a8b0198 100644 --- a/src/helper/compose.ts +++ b/src/compose.ts @@ -30,7 +30,7 @@ function dispatch( stack.depth = idx; - let maybePromise: Promise | void; + let maybePromise: Promise | void | undefined; if (idx < middlewareList.length) { const middleware = middlewareList[idx]; diff --git a/src/decorator.ts b/src/decorator.ts index 57dafe8..5d514bf 100644 --- a/src/decorator.ts +++ b/src/decorator.ts @@ -11,7 +11,7 @@ import { IAfterReturningAspectHookFunction, IAfterThrowingAspectHookFunction, IAroundHookOptions, -} from './declare'; +} from './types'; import { addDeps, getInjectorOfInstance, @@ -26,7 +26,7 @@ import { import { noInjectorError, notInjectError, tokenInvalidError } from './error'; /** - * 装饰一个 Class 是否是可以被依赖注入 + * Decorate a Class to mark it as injectable * @param opts */ export function Injectable(opts?: InstanceOpts): ClassDecorator { @@ -55,7 +55,7 @@ export function Injectable(opts?: InstanceOpts): ClassDecorator { interface InjectOpts { /** - * 默认值 + * Default value when the token is not found */ default?: any; } @@ -97,7 +97,7 @@ export function Autowired(token?: Token, opts?: InstanceOpts): PropertyDecorator throw tokenInvalidError(target, propertyKey, realToken); } - // 添加构造函数的依赖 + // Add the dependency of the constructor addDeps(target, realToken); const descriptor: PropertyDescriptor = { diff --git a/src/error.ts b/src/error.ts index 5087efd..181fe8c 100644 --- a/src/error.ts +++ b/src/error.ts @@ -1,4 +1,4 @@ -import { Context, Token } from './declare'; +import { Context, Token } from './types'; function stringify(target: object | Token) { if (typeof target === 'object') { diff --git a/src/factoryHelper.ts b/src/factoryHelper.ts index f43de3f..d620bc8 100644 --- a/src/factoryHelper.ts +++ b/src/factoryHelper.ts @@ -1,4 +1,4 @@ -import { FactoryFunction } from './declare'; +import { FactoryFunction } from './types'; import type { Injector } from './injector'; diff --git a/src/helper/dep-helper.ts b/src/helper/dep-helper.ts index 9fd04a6..d9799d5 100644 --- a/src/helper/dep-helper.ts +++ b/src/helper/dep-helper.ts @@ -1,5 +1,5 @@ -import { flatten, uniq } from './util'; -import { Token } from '../declare'; +import { flatten, uniq } from './utils'; +import { Token } from '../types'; import { getParameterDeps } from './parameter-helper'; import { createConstructorMetadataManager } from './reflect-helper'; diff --git a/src/helper/hook-helper.ts b/src/helper/hook-helper.ts index 7f1ec91..135746d 100644 --- a/src/helper/hook-helper.ts +++ b/src/helper/hook-helper.ts @@ -22,8 +22,8 @@ import { IAfterThrowingJoinPoint, IInstanceHooks, InstanceCreator, -} from '../declare'; -import compose, { Middleware } from './compose'; +} from '../types'; +import compose, { Middleware } from '../compose'; export const HOOKED_SYMBOL = Symbol('COMMON_DI_HOOKED'); diff --git a/src/helper/index.ts b/src/helper/index.ts index 5bbaaef..f3345e6 100644 --- a/src/helper/index.ts +++ b/src/helper/index.ts @@ -2,6 +2,6 @@ export * from './dep-helper'; export * from './injector-helper'; export * from './parameter-helper'; export * from './provider-helper'; -export * from './util'; +export * from './utils'; export * from './is-function'; export * from './hook-helper'; diff --git a/src/helper/injector-helper.ts b/src/helper/injector-helper.ts index 370a2f9..365106c 100644 --- a/src/helper/injector-helper.ts +++ b/src/helper/injector-helper.ts @@ -1,5 +1,5 @@ import 'reflect-metadata'; -import { InstanceOpts } from '../declare'; +import { InstanceOpts } from '../types'; import type { Injector } from '../injector'; import { VERSION } from '../constants'; diff --git a/src/helper/is-function.ts b/src/helper/is-function.ts index 24d0ce3..a0572d0 100644 --- a/src/helper/is-function.ts +++ b/src/helper/is-function.ts @@ -12,7 +12,7 @@ import { CreatorStatus, AliasProvider, AliasCreator, -} from '../declare'; +} from '../types'; import { isInjectable } from './injector-helper'; export function isTypeProvider(provider: Provider | Token): provider is TypeProvider { diff --git a/src/helper/parameter-helper.ts b/src/helper/parameter-helper.ts index 31455b6..44526d3 100644 --- a/src/helper/parameter-helper.ts +++ b/src/helper/parameter-helper.ts @@ -1,4 +1,4 @@ -import { Token, ParameterOpts } from '../declare'; +import { Token, ParameterOpts } from '../types'; import { createConstructorMetadataManager } from './reflect-helper'; const PARAMETER_KEY = Symbol('PARAMETER_KEY'); diff --git a/src/helper/provider-helper.ts b/src/helper/provider-helper.ts index 9305f57..2bb59f1 100644 --- a/src/helper/provider-helper.ts +++ b/src/helper/provider-helper.ts @@ -1,4 +1,4 @@ -import { Provider, Token, Tag, InstanceCreator, CreatorStatus, InstanceOpts } from '../declare'; +import { Provider, Token, Tag, InstanceCreator, CreatorStatus, InstanceOpts } from '../types'; import { isValueProvider, isClassProvider, diff --git a/src/helper/util.ts b/src/helper/utils.ts similarity index 100% rename from src/helper/util.ts rename to src/helper/utils.ts diff --git a/src/index.ts b/src/index.ts index 71a8e87..157c455 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -export * from './declare'; +export * from './types'; export * from './decorator'; export * from './injector'; export * from './factoryHelper'; diff --git a/src/injector.ts b/src/injector.ts index 41b7855..0835157 100644 --- a/src/injector.ts +++ b/src/injector.ts @@ -32,7 +32,7 @@ import { ParameterOpts, IDisposable, FactoryCreator, -} from './declare'; +} from './types'; import { isClassCreator, setInjector, diff --git a/src/declare.ts b/src/types.ts similarity index 100% rename from src/declare.ts rename to src/types.ts diff --git a/test/helper/compose.test.ts b/test/helper/compose.test.ts index 686f5ca..25fc69e 100644 --- a/test/helper/compose.test.ts +++ b/test/helper/compose.test.ts @@ -1,4 +1,4 @@ -import compose, { Middleware } from '../../src/helper/compose'; +import compose, { Middleware } from '../../src/compose'; interface ExampleContext { getName(): string; diff --git a/test/helper/util-helper.test.ts b/test/helper/util-helper.test.ts index cd1a06e..c6476b9 100644 --- a/test/helper/util-helper.test.ts +++ b/test/helper/util-helper.test.ts @@ -1,4 +1,4 @@ -import * as Helper from '../../src/helper/util'; +import * as Helper from '../../src/helper/utils'; describe(__filename, () => { it('uniq', () => { From 9374d7140d65559d0424bdcb9f4b4fa5ae3c31aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=87=8E=E5=A3=B0?= Date: Mon, 4 Dec 2023 10:55:23 +0800 Subject: [PATCH 12/25] refactor: remove reflect-metadata --- jest.config.js | 15 +++++++++++++++ package.json | 22 ++-------------------- scripts/jest-setup.ts | 1 + src/decorator.ts | 1 - src/helper/hook-helper.ts | 1 - src/helper/injector-helper.ts | 1 - src/helper/reflect-helper.ts | 2 -- src/typings.d.ts | 1 + 8 files changed, 19 insertions(+), 25 deletions(-) create mode 100644 jest.config.js create mode 100644 scripts/jest-setup.ts create mode 100644 src/typings.d.ts diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..40e8046 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,15 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + coveragePathIgnorePatterns: ['/node_modules/', '/test/'], + coverageThreshold: { + global: { + branches: 10, + functions: 10, + lines: 10, + statements: 10, + }, + }, + setupFilesAfterEnv: ['/scripts/jest-setup.ts'], +}; diff --git a/package.json b/package.json index cb9e682..46043fd 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,6 @@ "release": "commit-and-tag-version --npmPublishHint 'echo Just Push code to remote repo, npm publish will be done by CI.'", "release:beta": "npm run release -- --prerelease beta" }, - "dependencies": { - "reflect-metadata": "^0.1.13" - }, "devDependencies": { "@commitlint/cli": "17.2.0", "@commitlint/config-conventional": "17.2.0", @@ -35,7 +32,8 @@ "lint-staged": "13.0.3", "prettier": "2.7.1", "ts-jest": "29.0.3", - "typescript": "4.8.4" + "typescript": "4.8.4", + "reflect-metadata": "^0.1.13" }, "repository": { "type": "git", @@ -55,22 +53,6 @@ "@commitlint/config-conventional" ] }, - "jest": { - "preset": "ts-jest", - "testEnvironment": "node", - "coveragePathIgnorePatterns": [ - "/node_modules/", - "/test/" - ], - "coverageThreshold": { - "global": { - "branches": 10, - "functions": 10, - "lines": 10, - "statements": 10 - } - } - }, "keywords": [ "di", "injector" diff --git a/scripts/jest-setup.ts b/scripts/jest-setup.ts new file mode 100644 index 0000000..d2c9bc6 --- /dev/null +++ b/scripts/jest-setup.ts @@ -0,0 +1 @@ +import 'reflect-metadata'; diff --git a/src/decorator.ts b/src/decorator.ts index 5d514bf..2022521 100644 --- a/src/decorator.ts +++ b/src/decorator.ts @@ -1,4 +1,3 @@ -import 'reflect-metadata'; import { Token, InstanceOpts, diff --git a/src/helper/hook-helper.ts b/src/helper/hook-helper.ts index 135746d..f01b30a 100644 --- a/src/helper/hook-helper.ts +++ b/src/helper/hook-helper.ts @@ -1,4 +1,3 @@ -import 'reflect-metadata'; import { IHookStore, IDisposable, diff --git a/src/helper/injector-helper.ts b/src/helper/injector-helper.ts index 365106c..36611c3 100644 --- a/src/helper/injector-helper.ts +++ b/src/helper/injector-helper.ts @@ -1,4 +1,3 @@ -import 'reflect-metadata'; import { InstanceOpts } from '../types'; import type { Injector } from '../injector'; import { VERSION } from '../constants'; diff --git a/src/helper/reflect-helper.ts b/src/helper/reflect-helper.ts index 158af40..48d092c 100644 --- a/src/helper/reflect-helper.ts +++ b/src/helper/reflect-helper.ts @@ -1,5 +1,3 @@ -import 'reflect-metadata'; - function findConstructor(target: object) { return typeof target === 'object' ? target.constructor : target; } diff --git a/src/typings.d.ts b/src/typings.d.ts new file mode 100644 index 0000000..d2c9bc6 --- /dev/null +++ b/src/typings.d.ts @@ -0,0 +1 @@ +import 'reflect-metadata'; From cdfc135d8dd3e6beb19c515855edcf33c392b72c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=87=8E=E5=A3=B0?= Date: Mon, 4 Dec 2023 20:27:37 +0800 Subject: [PATCH 13/25] chore: update comment --- src/decorator.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/decorator.ts b/src/decorator.ts index 2022521..f074471 100644 --- a/src/decorator.ts +++ b/src/decorator.ts @@ -36,12 +36,12 @@ export function Injectable(opts?: InstanceOpts): ClassDecorator { if (Array.isArray(params)) { setParameters(target, params); - // 如果支持多例创建,就不检查构造函数依赖的可注入性 + // If it supports multiple instances, do not check the injectability of the constructor dependencies if (opts && opts.multiple) { return; } - // 检查依赖的可注入性 + // Check the injectability of the constructor dependencies const depTokens = getParameterDeps(target); depTokens.forEach((item, index) => { if (!isToken(item)) { From 5f13619eb644fa04ad486c511bcf8a18229dee5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=87=8E=E5=A3=B0?= Date: Mon, 4 Dec 2023 22:20:55 +0800 Subject: [PATCH 14/25] feat: hooks support priority option --- package.json | 4 +- src/helper/hook-helper.ts | 79 +++++++++++++-------------- src/injector.ts | 1 + src/types.ts | 19 +++++-- test/decorators/hooks.test.ts | 100 +++++++++++++++++++++++++++++++++- 5 files changed, 153 insertions(+), 50 deletions(-) diff --git a/package.json b/package.json index 46043fd..9dde0a2 100644 --- a/package.json +++ b/package.json @@ -31,9 +31,9 @@ "jest": "29.2.2", "lint-staged": "13.0.3", "prettier": "2.7.1", + "reflect-metadata": "^0.1.13", "ts-jest": "29.0.3", - "typescript": "4.8.4", - "reflect-metadata": "^0.1.13" + "typescript": "4.8.4" }, "repository": { "type": "git", diff --git a/src/helper/hook-helper.ts b/src/helper/hook-helper.ts index f01b30a..1d04ce2 100644 --- a/src/helper/hook-helper.ts +++ b/src/helper/hook-helper.ts @@ -24,7 +24,7 @@ import { } from '../types'; import compose, { Middleware } from '../compose'; -export const HOOKED_SYMBOL = Symbol('COMMON_DI_HOOKED'); +export const HOOKED_SYMBOL = Symbol('DI_HOOKED'); export function applyHooks(instance: T, token: Token, hooks: IHookStore): T { if (typeof instance !== 'object') { @@ -75,6 +75,7 @@ export function createHookedFunction( const aroundHooks: Array> = []; const afterReturningHooks: Array> = []; const afterThrowingHooks: Array> = []; + // Onion model hooks.forEach((h) => { if (isBeforeHook(h)) { @@ -119,27 +120,18 @@ export function createHookedFunction( } finally { if (error) { // noop + } else if (promise) { + promise.then( + () => { + runAfterReturning(); + }, + (e) => { + error = e; + runAfterThrowing(); + }, + ); } else { - // 异步逻辑 - let p: Promise | undefined; - if (promise) { - p = promise; - } - - if (p) { - // p is a promise, use Promise's reject and resolve at this time - p.then( - () => { - runAfterReturning(); - }, - (e) => { - error = e; - runAfterThrowing(); - }, - ); - } else { - runAfterReturning(); - } + runAfterReturning(); } } @@ -218,6 +210,7 @@ export function createHookedFunction( if (afterHooks.length === 0) { return; } + let _inThisHook = true; const afterJoinPoint: IAfterJoinPoint = { getArgs: () => { @@ -252,6 +245,7 @@ export function createHookedFunction( if (afterReturningHooks.length === 0) { return; } + const afterReturningJoinPoint: IAfterReturningJoinPoint = { getArgs: () => { return args; @@ -283,6 +277,7 @@ export function createHookedFunction( if (afterThrowingHooks.length === 0) { return; } + const afterThrowingJoinPoint: IAfterThrowingJoinPoint = { getError: () => { return error; @@ -395,17 +390,15 @@ function runOneHook< if (hook.awaitPromise) { promise = promise || Promise.resolve(); promise = promise.then(() => { + // notice: here we return hook's result, and the return statement on the next will return undefined. return hook.hook(joinPoint); }); - } else { - if (promise) { - promise = promise.then(() => { - hook.hook(joinPoint); - return; - }); - } else { + } else if (promise) { + promise = promise.then(() => { hook.hook(joinPoint); - } + }); + } else { + hook.hook(joinPoint); } return promise; } @@ -421,33 +414,37 @@ export class HookStore implements IHookStore { }); return { dispose: () => { - disposers.forEach((disposer) => { - disposer.dispose(); - }); + for (const toDispose of disposers) { + toDispose.dispose(); + } }, }; } hasHooks(token: Token) { - if (!this.hooks.has(token)) { - if (this.parent) { - return this.parent.hasHooks(token); - } else { - return false; - } - } else { + if (this.hooks.has(token)) { return true; } + if (this.parent) { + return this.parent.hasHooks(token); + } + return false; } getHooks(token: Token, method: string | number | symbol): IValidAspectHook[] { let hooks: IValidAspectHook[] = []; if (this.parent) { - hooks = this.parent.getHooks(token, method); + hooks = hooks.concat(this.parent.getHooks(token, method)); } + if (this.hooks.get(token)?.has(method)) { hooks = hooks.concat(this.hooks.get(token)!.get(method)!); } + + hooks.sort((a, b) => { + return (b.priority || 0) - (a.priority || 0); + }); + return hooks; } @@ -460,7 +457,6 @@ export class HookStore implements IHookStore { if (!instanceHooks.has(hook.method)) { instanceHooks.set(hook.method, []); } - // TODO: 支持order instanceHooks.get(hook.method)!.push(hook); return { dispose: () => { @@ -516,6 +512,7 @@ export function markAsHook( } hooks.push({ prop, type, target: hookTarget, targetMethod, options }); } + export function isAspectCreator(target: InstanceCreator) { return !!Reflect.getMetadata(ASPECT_KEY, (target as ClassCreator).useClass); } diff --git a/src/injector.ts b/src/injector.ts index 0835157..8cf664d 100644 --- a/src/injector.ts +++ b/src/injector.ts @@ -393,6 +393,7 @@ export class Injector { }; return { awaitPromise: metadata.options.await, + priority: metadata.options.priority || 0, hook: wrapped, method: metadata.targetMethod, target: metadata.target, diff --git a/src/types.ts b/src/types.ts index 6f45220..b52ebc7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -168,6 +168,7 @@ export interface IAspectHook; } @@ -256,15 +257,23 @@ export interface IDisposable { } export interface IHookOptions { - // Whether to wait for the hook (if the return value of the hook is a promise) + /** + * Whether to wait for the hook (if the return value of the hook is a promise) + */ await?: boolean; + + /** + * The priority of the hook. + * for `before` hooks, the higher the priority, the earlier the execution. + * for `after` and `around` hooks, the higher the priority, the later the execution. + * @default 0 + */ + priority?: number; } -export interface IAroundHookOptions { +export interface IAroundHookOptions extends IHookOptions { /** - * @deprecated AroundHook will always await the promise, it act as the union model. - * - * Whether to wait for the hook (if the return value of the hook is a promise) + * @deprecated around hooks act as the union model, you can just use `ctx.proceed()`(no await) to invoke the next hook. */ await?: boolean; } diff --git a/test/decorators/hooks.test.ts b/test/decorators/hooks.test.ts index 537bb35..172c6e9 100644 --- a/test/decorators/hooks.test.ts +++ b/test/decorators/hooks.test.ts @@ -17,7 +17,7 @@ import { } from '../../src'; describe('hook', () => { - it('使用代码来创建hook', async () => { + it('could use code to create hook', async () => { const injector = new Injector(); @Injectable() class TestClass { @@ -172,7 +172,7 @@ describe('hook', () => { expect(await testClass5.add(1, 2)).toBe(5); }); - it('使用注解来创建hook', async () => { + it('could use decorator to create hook', async () => { const TestClassToken = Symbol(); const pendings: Array> = []; @@ -394,6 +394,102 @@ describe('hook', () => { await Promise.all(pendings); }); + describe('hook priority', () => { + it("hook's priority should work case1", async () => { + @Injectable() + class TestClass { + async add(a: number, b: number): Promise { + return a + b; + } + } + const injector = new Injector(); + + const resultArray = [] as number[]; + + injector.createHook({ + hook: async (joinPoint) => { + resultArray.push(1); + joinPoint.proceed(); + resultArray.push(2); + return joinPoint.setResult(9); + }, + method: 'add', + target: TestClass, + type: HookType.Around, + priority: 1001, + }); + + injector.createHooks([ + { + hook: async (joinPoint) => { + resultArray.push(0); + joinPoint.proceed(); + resultArray.push(3); + const result = await joinPoint.getResult(); + if (result === 9) { + return joinPoint.setResult(10); + } + }, + method: 'add', + target: TestClass, + type: HookType.Around, + priority: 10011, + }, + ]); + + const testClass = injector.get(TestClass); + expect(await testClass.add(1, 2)).toBe(9); + expect(resultArray).toEqual([1, 0, 3, 2]); + }); + + it("hook's priority should work case2", async () => { + @Injectable() + class TestClass { + async add(a: number, b: number): Promise { + return a + b; + } + } + const injector = new Injector(); + + const resultArray = [] as number[]; + + injector.createHook({ + hook: async (joinPoint) => { + resultArray.push(1); + joinPoint.proceed(); + resultArray.push(2); + return joinPoint.setResult(9); + }, + method: 'add', + target: TestClass, + type: HookType.Around, + priority: 1001, + }); + + injector.createHooks([ + { + hook: async (joinPoint) => { + resultArray.push(0); + joinPoint.proceed(); + resultArray.push(3); + const result = await joinPoint.getResult(); + if (result === 9) { + return joinPoint.setResult(10); + } + }, + method: 'add', + target: TestClass, + type: HookType.Around, + priority: 100, + }, + ]); + + const testClass = injector.get(TestClass); + expect(await testClass.add(1, 2)).toBe(10); + expect(resultArray).toEqual([0, 1, 2, 3]); + }); + }); + it('子injector应该正确拦截', () => { @Injectable() class TestClass { From 2a5df3d85f961acb6ff5bc69e7db51ab609468e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=87=8E=E5=A3=B0?= Date: Mon, 4 Dec 2023 22:29:53 +0800 Subject: [PATCH 15/25] build: build esm format dist --- .gitignore | 4 +++- package.json | 7 +++++-- tsconfig.esm.json | 10 ++++++++++ tsconfig.json | 4 ++-- tsconfig.prod.json => tsconfig.lib.json | 5 +++-- 5 files changed, 23 insertions(+), 7 deletions(-) create mode 100644 tsconfig.esm.json rename tsconfig.prod.json => tsconfig.lib.json (55%) diff --git a/.gitignore b/.gitignore index 5ea00d9..d7ae9f5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ -# 由 https://github.com/msfeldstein/gitignore 自动生成 +lib +esm + # Logs logs *.log diff --git a/package.json b/package.json index 9dde0a2..05e5ffd 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,14 @@ "version": "1.10.0", "description": "A dependency injection tool for Javascript.", "license": "MIT", - "main": "dist/index.js", + "module": "esm/index.js", + "main": "lib/index.js", "scripts": { "prepare": "husky install", "lint": "eslint . --ext .js,.jsx,.ts,.tsx", - "build": "rm -rf dist && tsc -p tsconfig.prod.json", + "build": "npm run build:lib && npm run build:esm", + "build:lib": "rm -rf lib && tsc -p tsconfig.lib.json", + "build:esm": "rm -rf esm && tsc -p tsconfig.esm.json", "test": "jest --coverage test/**", "test:watch": "yarn test --watch", "ci": "npm run lint && npm run test", diff --git a/tsconfig.esm.json b/tsconfig.esm.json new file mode 100644 index 0000000..78fda11 --- /dev/null +++ b/tsconfig.esm.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "esm", + "module": "es2022", + "target": "es2022", + "lib": ["es2022"] + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json index 398993f..bb4d876 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,10 +10,10 @@ "pretty": false, "noEmitOnError": false, "noFallthroughCasesInSwitch": true, - "downlevelIteration": true, + "downlevelIteration": false, "esModuleInterop": true, "skipLibCheck": true, - "outDir": "dist" + "moduleResolution": "node" }, "include": ["src", "test"] } diff --git a/tsconfig.prod.json b/tsconfig.lib.json similarity index 55% rename from tsconfig.prod.json rename to tsconfig.lib.json index ee2af5e..7984282 100644 --- a/tsconfig.prod.json +++ b/tsconfig.lib.json @@ -1,8 +1,9 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "target": "es5", - "lib": ["es2017"] + "target": "es2015", + "lib": ["es2015"], + "outDir": "lib" }, "include": ["src"] } From 2201f130a527b8a3d6e2a5f9dd34cb434c74662d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=87=8E=E5=A3=B0?= Date: Tue, 5 Dec 2023 10:43:25 +0800 Subject: [PATCH 16/25] chore: update code --- src/helper/hook-helper.ts | 3 ++- src/injector.ts | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/helper/hook-helper.ts b/src/helper/hook-helper.ts index 1d04ce2..5efb494 100644 --- a/src/helper/hook-helper.ts +++ b/src/helper/hook-helper.ts @@ -390,7 +390,8 @@ function runOneHook< if (hook.awaitPromise) { promise = promise || Promise.resolve(); promise = promise.then(() => { - // notice: here we return hook's result, and the return statement on the next will return undefined. + // notice: here we return hook's result + // and the return statement on the next condition branch will return undefined. return hook.hook(joinPoint); }); } else if (promise) { diff --git a/src/injector.ts b/src/injector.ts index 8cf664d..3a372d0 100644 --- a/src/injector.ts +++ b/src/injector.ts @@ -386,19 +386,19 @@ export class Injector { return instance; }; - const preprocessedHooks: IValidAspectHook[] = hookMetadata.map((metadata) => { + const preprocessedHooks = hookMetadata.map((metadata) => { const wrapped = (...args: any[]) => { const instance = getInstance(); return instance[metadata.prop].call(instance, ...args); }; return { awaitPromise: metadata.options.await, - priority: metadata.options.priority || 0, + priority: metadata.options.priority, hook: wrapped, method: metadata.targetMethod, target: metadata.target, type: metadata.type, - }; + } as IValidAspectHook; }); toDispose = this.hookStore.createHooks(preprocessedHooks); } From d32e04137df491282845eb194fe341d16af871ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=87=8E=E5=A3=B0?= Date: Thu, 7 Dec 2023 18:23:47 +0800 Subject: [PATCH 17/25] refactor: use @opensumi/events --- package.json | 3 ++ src/helper/event.ts | 43 ------------------------ src/injector.ts | 2 +- test/helper/event.test.ts | 70 --------------------------------------- 4 files changed, 4 insertions(+), 114 deletions(-) delete mode 100644 src/helper/event.ts delete mode 100644 test/helper/event.test.ts diff --git a/package.json b/package.json index 05e5ffd..04ad21f 100644 --- a/package.json +++ b/package.json @@ -78,5 +78,8 @@ "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" + }, + "dependencies": { + "@opensumi/events": "^0.0.6" } } diff --git a/src/helper/event.ts b/src/helper/event.ts deleted file mode 100644 index 9ec7db7..0000000 --- a/src/helper/event.ts +++ /dev/null @@ -1,43 +0,0 @@ -export class EventEmitter { - private _listeners: Map = new Map(); - - on(event: T, listener: Function) { - if (!this._listeners.has(event)) { - this._listeners.set(event, []); - } - this._listeners.get(event)!.push(listener); - - return () => this.off(event, listener); - } - - off(event: T, listener: Function) { - if (!this._listeners.has(event)) { - return; - } - const listeners = this._listeners.get(event)!; - const index = listeners.indexOf(listener); - if (index !== -1) { - listeners.splice(index, 1); - } - } - - once(event: T, listener: Function) { - const remove: () => void = this.on(event, (...args: any[]) => { - remove(); - listener.apply(this, args); - }); - - return remove; - } - - emit(event: T, ...args: any[]) { - if (!this._listeners.has(event)) { - return; - } - [...this._listeners.get(event)!].forEach((listener) => listener.apply(this, args)); - } - - dispose() { - this._listeners.clear(); - } -} diff --git a/src/injector.ts b/src/injector.ts index 3a372d0..5d6a825 100644 --- a/src/injector.ts +++ b/src/injector.ts @@ -44,7 +44,7 @@ import { getHookMeta, isAliasCreator, } from './helper'; -import { EventEmitter } from './helper/event'; +import { EventEmitter } from '@opensumi/events'; export class Injector { id = injectorIdGenerator.next(); diff --git a/test/helper/event.test.ts b/test/helper/event.test.ts deleted file mode 100644 index 1e06a6e..0000000 --- a/test/helper/event.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { EventEmitter } from '../../src/helper/event'; - -describe('event emitter', () => { - it('basic usage', () => { - const emitter = new EventEmitter(); - const spy = jest.fn(); - const spy2 = jest.fn(); - emitter.on('test', spy); - emitter.on('foo', spy2); - emitter.emit('test', 'hello'); - expect(spy).toBeCalledWith('hello'); - emitter.off('test', spy); - emitter.emit('test', 'hello'); - expect(spy).toBeCalledTimes(1); - - emitter.once('test', spy); - emitter.emit('test', 'hello'); - expect(spy).toBeCalledTimes(2); - emitter.emit('test', 'hello'); - expect(spy).toBeCalledTimes(2); - - emitter.off('bar', spy); - - emitter.dispose(); - - emitter.emit('test', 'hello'); - expect(spy).toBeCalledTimes(2); - }); - - it('many listeners listen to one event', () => { - const emitter = new EventEmitter(); - const spy = jest.fn(); - const spy2 = jest.fn(); - emitter.on('test', spy); - emitter.on('test', spy2); - emitter.emit('test', 'hello'); - expect(spy).toBeCalledWith('hello'); - expect(spy2).toBeCalledWith('hello'); - - emitter.off('test', spy); - emitter.emit('test', 'hello'); - expect(spy).toBeCalledTimes(1); - expect(spy2).toBeCalledTimes(2); - - emitter.dispose(); - }); - - it('can dispose event listener by using returned function', () => { - const emitter = new EventEmitter(); - const spy = jest.fn(); - const spy2 = jest.fn(); - const spy3 = jest.fn(); - const disposeSpy = emitter.on('test', spy); - emitter.on('test', spy2); - - const disposeSpy3 = emitter.once('test', spy3); - disposeSpy3(); - - emitter.emit('test', 'hello'); - expect(spy).toBeCalledWith('hello'); - expect(spy2).toBeCalledWith('hello'); - - disposeSpy(); - emitter.emit('test', 'hello'); - expect(spy).toBeCalledTimes(1); - expect(spy2).toBeCalledTimes(2); - expect(spy3).toBeCalledTimes(0); - emitter.dispose(); - }); -}); From b3f4361582eaccc92a7c339f6020b4447b9c6172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=87=8E=E5=A3=B0?= Date: Thu, 7 Dec 2023 18:26:11 +0800 Subject: [PATCH 18/25] build: add types dir --- .gitignore | 1 + package.json | 1 + tsconfig.esm.json | 4 +++- tsconfig.json | 1 - tsconfig.lib.json | 3 ++- 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index d7ae9f5..f75d46a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ lib esm +types # Logs logs diff --git a/package.json b/package.json index 04ad21f..87b3ae1 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "license": "MIT", "module": "esm/index.js", "main": "lib/index.js", + "types": "types/index.d.ts", "scripts": { "prepare": "husky install", "lint": "eslint . --ext .js,.jsx,.ts,.tsx", diff --git a/tsconfig.esm.json b/tsconfig.esm.json index 78fda11..0ee6f8f 100644 --- a/tsconfig.esm.json +++ b/tsconfig.esm.json @@ -4,7 +4,9 @@ "outDir": "esm", "module": "es2022", "target": "es2022", - "lib": ["es2022"] + "lib": ["es2022"], + "declaration": true, + "declarationDir": "types" }, "include": ["src"] } diff --git a/tsconfig.json b/tsconfig.json index bb4d876..599fbdb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,6 @@ "module": "commonjs", "lib": ["es2020"], "strict": true, - "declaration": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "pretty": false, diff --git a/tsconfig.lib.json b/tsconfig.lib.json index 7984282..8acda50 100644 --- a/tsconfig.lib.json +++ b/tsconfig.lib.json @@ -3,7 +3,8 @@ "compilerOptions": { "target": "es2015", "lib": ["es2015"], - "outDir": "lib" + "outDir": "lib", + "declaration": false }, "include": ["src"] } From 636ff253f1c29476c5dcf8084a024f53269fe0c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=87=8E=E5=A3=B0?= Date: Thu, 7 Dec 2023 18:34:07 +0800 Subject: [PATCH 19/25] refactor: throw if no instances --- src/error.ts | 4 ++++ src/injector.ts | 23 ++++++++++++++++++----- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/error.ts b/src/error.ts index 181fe8c..3024884 100644 --- a/src/error.ts +++ b/src/error.ts @@ -65,3 +65,7 @@ export function aliasCircularError(paths: Token[], current: Token) { `useAlias registration cycle detected! ${[...paths, current].map((v) => stringify(v)).join(' -> ')}`, ); } + +export function noInstancesInCompletedCreatorError(token: Token) { + return new Error(`Cannot find value of ${stringify(token)} in a completed creator.`); +} diff --git a/src/injector.ts b/src/injector.ts index 5d6a825..7b30dce 100644 --- a/src/injector.ts +++ b/src/injector.ts @@ -11,7 +11,13 @@ import { parseTokenFromProvider, uniq, } from './helper'; -import { aliasCircularError, circularError, noProviderError, tagOnlyError } from './error'; +import { + aliasCircularError, + circularError, + noProviderError, + noInstancesInCompletedCreatorError, + tagOnlyError, +} from './error'; import { INJECTOR_TOKEN, Provider, @@ -457,8 +463,12 @@ export class Injector { if (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.instances!.values().next().value; + if (creator.status === CreatorStatus.done && !opts.multiple) { + if (creator.instances) { + return creator.instances.values().next().value; + } + + throw noInstancesInCompletedCreatorError(ctx.token); } return this.createInstanceFromClassCreator(ctx as Context, opts, args); @@ -468,8 +478,11 @@ export class Injector { return this.createInstanceFromFactory(ctx as Context); } - // must be ValueCreator, no need to hook. - return creator.instances!.values().next().value; + if (creator.instances) { + return creator.instances.values().next().value; + } + + throw noInstancesInCompletedCreatorError(ctx.token); } private createInstanceFromClassCreator(ctx: Context, opts: InstanceOpts, defaultArgs?: any[]) { From 477877d2f2b342d3b58ab4effa7d7284acea61de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=87=8E=E5=A3=B0?= Date: Thu, 7 Dec 2023 18:40:04 +0800 Subject: [PATCH 20/25] test: update testcase name --- test/decorators/Autowired.test.ts | 6 +++--- test/decorators/hooks.test.ts | 11 +++++------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/test/decorators/Autowired.test.ts b/test/decorators/Autowired.test.ts index 3627dd4..34fdbe5 100644 --- a/test/decorators/Autowired.test.ts +++ b/test/decorators/Autowired.test.ts @@ -12,7 +12,7 @@ describe('Autowired decorator', () => { return B; }).toThrow(); }); - it('进行依赖注入的时候,没有定义 Token 会报错', () => { + it('will throw error if the Token is not defined, when performing dependency injection.', () => { expect(() => { interface A { log(): void; @@ -26,7 +26,7 @@ describe('Autowired decorator', () => { }).toThrow(Error.tokenInvalidError(class B {}, 'a', Object)); }); - it('使用 null 进行依赖定义,期望报错', () => { + it('define dependencies with null, expect an error', () => { expect(() => { interface A { log(): void; @@ -40,7 +40,7 @@ describe('Autowired decorator', () => { }).toThrow(); }); - it('使用原始 Number 进行依赖定义,期望报错', () => { + it('Define dependencies using the original Number, expect an error', () => { expect(() => { interface A { log(): void; diff --git a/test/decorators/hooks.test.ts b/test/decorators/hooks.test.ts index 172c6e9..a74ab68 100644 --- a/test/decorators/hooks.test.ts +++ b/test/decorators/hooks.test.ts @@ -74,14 +74,14 @@ describe('hook', () => { hook: () => undefined, method: 'add', target: TestClass, - type: 'other' as any, // 不会造成任何影响(为了提高覆盖率) + type: 'other' as any, // for coverage }); const testClass = injector.get(TestClass); expect(testClass.add(1, 2)).toBe(5); expect(testClass.add(3, 4)).toBe(9); - // 同步变成异步 // Async hook on sync target + // the sync method will be wrapped to async method injector.createHook({ awaitPromise: true, hook: async (joinPoint: IBeforeJoinPoint) => { @@ -490,7 +490,7 @@ describe('hook', () => { }); }); - it('子injector应该正确拦截', () => { + it('can work in child injector', () => { @Injectable() class TestClass { add(a: number, b: number): number { @@ -560,8 +560,8 @@ describe('hook', () => { 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都会命中 + expect(testClassChild.add(1, 2)).toBe(30); // on hooked by parent + expect(testClassChild2.add(1, 2)).toBe(600); // will hooked by parent and child }); it('could dispose proxied instance', () => { @@ -623,7 +623,6 @@ describe('hook', () => { expect(constructorSpy).toBeCalledTimes(1); injector.disposeOne(TestClass); - // injector.disposeOne(TestAspect); expect(example.run()).toBe(30); expect(constructorSpy).toBeCalledTimes(2); From 501d20a39334d2dd7272fcbfcd26956e4b414de1 Mon Sep 17 00:00:00 2001 From: artin Date: Thu, 14 Dec 2023 10:35:59 +0800 Subject: [PATCH 21/25] test: ignore impossible path coverage --- package.json | 2 +- src/error.ts | 1 + src/injector.ts | 2 ++ {test => tests}/aspect.test.ts | 0 {test => tests}/cases/issue-115/TestClass.ts | 0 {test => tests}/cases/issue-115/const.ts | 0 {test => tests}/cases/issue-115/index.test.ts | 0 {test => tests}/decorator.test.ts | 0 {test => tests}/decorators/Autowired.test.ts | 0 {test => tests}/decorators/hooks.test.ts | 0 {test => tests}/helper/compose.test.ts | 0 {test => tests}/helper/dep-helper.test.ts | 14 +++++++------- {test => tests}/helper/hook-helper.test.ts | 0 {test => tests}/helper/injector-helper.test.ts | 0 {test => tests}/helper/is-function.test.ts | 0 {test => tests}/helper/parameter-helper.test.ts | 14 +++++++------- {test => tests}/helper/provider-helper.test.ts | 0 {test => tests}/helper/reflect-helper.test.ts | 0 {test => tests}/helper/util-helper.test.ts | 0 {test => tests}/injector.test.ts | 0 {test => tests}/injector/dispose.test.ts | 0 {test => tests}/injector/domain.test.ts | 0 {test => tests}/injector/dynamicMultiple.test.ts | 0 {test => tests}/injector/hasInstance.test.ts | 0 {test => tests}/injector/overrideProviders.test.ts | 0 {test => tests}/injector/strictMode.test.ts | 0 {test => tests}/injector/tag.test.ts | 0 {test => tests}/providers/useAlias.test.ts | 0 {test => tests}/use-case.test.ts | 0 tsconfig.json | 2 +- 30 files changed, 19 insertions(+), 16 deletions(-) rename {test => tests}/aspect.test.ts (100%) rename {test => tests}/cases/issue-115/TestClass.ts (100%) rename {test => tests}/cases/issue-115/const.ts (100%) rename {test => tests}/cases/issue-115/index.test.ts (100%) rename {test => tests}/decorator.test.ts (100%) rename {test => tests}/decorators/Autowired.test.ts (100%) rename {test => tests}/decorators/hooks.test.ts (100%) rename {test => tests}/helper/compose.test.ts (100%) rename {test => tests}/helper/dep-helper.test.ts (84%) rename {test => tests}/helper/hook-helper.test.ts (100%) rename {test => tests}/helper/injector-helper.test.ts (100%) rename {test => tests}/helper/is-function.test.ts (100%) rename {test => tests}/helper/parameter-helper.test.ts (84%) rename {test => tests}/helper/provider-helper.test.ts (100%) rename {test => tests}/helper/reflect-helper.test.ts (100%) rename {test => tests}/helper/util-helper.test.ts (100%) rename {test => tests}/injector.test.ts (100%) rename {test => tests}/injector/dispose.test.ts (100%) rename {test => tests}/injector/domain.test.ts (100%) rename {test => tests}/injector/dynamicMultiple.test.ts (100%) rename {test => tests}/injector/hasInstance.test.ts (100%) rename {test => tests}/injector/overrideProviders.test.ts (100%) rename {test => tests}/injector/strictMode.test.ts (100%) rename {test => tests}/injector/tag.test.ts (100%) rename {test => tests}/providers/useAlias.test.ts (100%) rename {test => tests}/use-case.test.ts (100%) diff --git a/package.json b/package.json index ec23fa8..a339c44 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "build": "npm run build:lib && npm run build:esm", "build:lib": "rm -rf lib && tsc -p tsconfig.lib.json", "build:esm": "rm -rf esm && tsc -p tsconfig.esm.json", - "test": "jest --coverage test/**", + "test": "jest --coverage tests/**", "test:watch": "yarn test --watch", "ci": "npm run lint && npm run test", "prepublishOnly": "npm run build", diff --git a/src/error.ts b/src/error.ts index 3024884..bb13d74 100644 --- a/src/error.ts +++ b/src/error.ts @@ -67,5 +67,6 @@ export function aliasCircularError(paths: Token[], current: Token) { } export function noInstancesInCompletedCreatorError(token: Token) { + /* istanbul ignore next */ return new Error(`Cannot find value of ${stringify(token)} in a completed creator.`); } diff --git a/src/injector.ts b/src/injector.ts index 8c0f2b9..4cb07ca 100644 --- a/src/injector.ts +++ b/src/injector.ts @@ -471,6 +471,7 @@ export class Injector { throw noInstancesInCompletedCreatorError(ctx.token); } + /* istanbul ignore next */ return this.createInstanceFromClassCreator(ctx as Context, opts, args); } @@ -482,6 +483,7 @@ export class Injector { return creator.instances.values().next().value; } + /* istanbul ignore next */ throw noInstancesInCompletedCreatorError(ctx.token); } diff --git a/test/aspect.test.ts b/tests/aspect.test.ts similarity index 100% rename from test/aspect.test.ts rename to tests/aspect.test.ts diff --git a/test/cases/issue-115/TestClass.ts b/tests/cases/issue-115/TestClass.ts similarity index 100% rename from test/cases/issue-115/TestClass.ts rename to tests/cases/issue-115/TestClass.ts diff --git a/test/cases/issue-115/const.ts b/tests/cases/issue-115/const.ts similarity index 100% rename from test/cases/issue-115/const.ts rename to tests/cases/issue-115/const.ts diff --git a/test/cases/issue-115/index.test.ts b/tests/cases/issue-115/index.test.ts similarity index 100% rename from test/cases/issue-115/index.test.ts rename to tests/cases/issue-115/index.test.ts diff --git a/test/decorator.test.ts b/tests/decorator.test.ts similarity index 100% rename from test/decorator.test.ts rename to tests/decorator.test.ts diff --git a/test/decorators/Autowired.test.ts b/tests/decorators/Autowired.test.ts similarity index 100% rename from test/decorators/Autowired.test.ts rename to tests/decorators/Autowired.test.ts diff --git a/test/decorators/hooks.test.ts b/tests/decorators/hooks.test.ts similarity index 100% rename from test/decorators/hooks.test.ts rename to tests/decorators/hooks.test.ts diff --git a/test/helper/compose.test.ts b/tests/helper/compose.test.ts similarity index 100% rename from test/helper/compose.test.ts rename to tests/helper/compose.test.ts diff --git a/test/helper/dep-helper.test.ts b/tests/helper/dep-helper.test.ts similarity index 84% rename from test/helper/dep-helper.test.ts rename to tests/helper/dep-helper.test.ts index c283ba0..0098e72 100644 --- a/test/helper/dep-helper.test.ts +++ b/tests/helper/dep-helper.test.ts @@ -24,7 +24,7 @@ describe(__filename, () => { Helper.addDeps(Constructor, dep); const depsFromConstructor = Helper.getAllDeps(Constructor); - expect(depsFromConstructor).toEqual([ dep ]); + expect(depsFromConstructor).toEqual([dep]); }); it('在父级进行依赖定义', () => { @@ -32,7 +32,7 @@ describe(__filename, () => { Helper.addDeps(Parent, dep); const depsFromConstructor = Helper.getAllDeps(Constructor); - expect(depsFromConstructor).toEqual([ dep ]); + expect(depsFromConstructor).toEqual([dep]); }); it('依赖取值出来应该是去重的结果', () => { @@ -40,7 +40,7 @@ describe(__filename, () => { Helper.addDeps(Parent, dep, dep); const depsFromConstructor = Helper.getAllDeps(Constructor); - expect(depsFromConstructor).toEqual([ dep ]); + expect(depsFromConstructor).toEqual([dep]); }); it('在父级进行依赖定义,并且再新定义', () => { @@ -51,10 +51,10 @@ describe(__filename, () => { Helper.addDeps(Constructor, dep2); const depsFromParentConstructor = Helper.getAllDeps(Parent); - expect(depsFromParentConstructor).toEqual([ dep1 ]); + expect(depsFromParentConstructor).toEqual([dep1]); const depsFromConstructor = Helper.getAllDeps(Constructor); - expect(depsFromConstructor).toEqual([ dep1, dep2 ]); + expect(depsFromConstructor).toEqual([dep1, dep2]); }); it('当前一个依赖包含了后面的所有依赖的时候,应该正确解析', () => { @@ -68,7 +68,7 @@ describe(__filename, () => { const deps = Helper.getAllDeps(Dep1, Dep2); const depsAgain = Helper.getAllDeps(Dep2); - expect(deps).toEqual([ Dep2, Dep3 ]); - expect(depsAgain).toEqual([ Dep3 ]); + expect(deps).toEqual([Dep2, Dep3]); + expect(depsAgain).toEqual([Dep3]); }); }); diff --git a/test/helper/hook-helper.test.ts b/tests/helper/hook-helper.test.ts similarity index 100% rename from test/helper/hook-helper.test.ts rename to tests/helper/hook-helper.test.ts diff --git a/test/helper/injector-helper.test.ts b/tests/helper/injector-helper.test.ts similarity index 100% rename from test/helper/injector-helper.test.ts rename to tests/helper/injector-helper.test.ts diff --git a/test/helper/is-function.test.ts b/tests/helper/is-function.test.ts similarity index 100% rename from test/helper/is-function.test.ts rename to tests/helper/is-function.test.ts diff --git a/test/helper/parameter-helper.test.ts b/tests/helper/parameter-helper.test.ts similarity index 84% rename from test/helper/parameter-helper.test.ts rename to tests/helper/parameter-helper.test.ts index e0f52f9..d365e6f 100644 --- a/test/helper/parameter-helper.test.ts +++ b/tests/helper/parameter-helper.test.ts @@ -54,28 +54,28 @@ describe(__filename, () => { }); it('从子类设置的依赖能够覆盖父类的依赖', () => { - Helper.setParameters(Parent, [ 'parent', 'parent' ]); + Helper.setParameters(Parent, ['parent', 'parent']); const parentDeps = Helper.getParameterDeps(Parent); - expect(parentDeps).toEqual([ 'parent', 'parent' ]); + expect(parentDeps).toEqual(['parent', 'parent']); - Helper.setParameters(Constructor, [ 'child', 'child' ]); + Helper.setParameters(Constructor, ['child', 'child']); const childDeps = Helper.getParameterDeps(Constructor); - expect(childDeps).toEqual([ 'child', 'child' ]); + expect(childDeps).toEqual(['child', 'child']); }); it('不同位置的 Token 描述能够合并', () => { Helper.setParameterIn(Parent, { token: 'parent' }, 0); Helper.setParameterIn(Constructor, { token: 'child' }, 1); const deps = Helper.getParameterDeps(Constructor); - expect(deps).toEqual([ 'parent', 'child' ]); + expect(deps).toEqual(['parent', 'child']); }); it('能够得到构造依赖和 Token 定义的结果产物', () => { - Helper.setParameters(Constructor, [ 'parameter1', 'parameter2' ]); + Helper.setParameters(Constructor, ['parameter1', 'parameter2']); Helper.setParameterIn(Constructor, { token: 'token' }, 1); const deps = Helper.getParameterDeps(Constructor); - expect(deps).toEqual([ 'parameter1', 'token' ]); + expect(deps).toEqual(['parameter1', 'token']); const opts = Helper.getParameterOpts(Constructor); expect(opts).toEqual([{ token: 'parameter1' }, { token: 'token' }]); diff --git a/test/helper/provider-helper.test.ts b/tests/helper/provider-helper.test.ts similarity index 100% rename from test/helper/provider-helper.test.ts rename to tests/helper/provider-helper.test.ts diff --git a/test/helper/reflect-helper.test.ts b/tests/helper/reflect-helper.test.ts similarity index 100% rename from test/helper/reflect-helper.test.ts rename to tests/helper/reflect-helper.test.ts diff --git a/test/helper/util-helper.test.ts b/tests/helper/util-helper.test.ts similarity index 100% rename from test/helper/util-helper.test.ts rename to tests/helper/util-helper.test.ts diff --git a/test/injector.test.ts b/tests/injector.test.ts similarity index 100% rename from test/injector.test.ts rename to tests/injector.test.ts diff --git a/test/injector/dispose.test.ts b/tests/injector/dispose.test.ts similarity index 100% rename from test/injector/dispose.test.ts rename to tests/injector/dispose.test.ts diff --git a/test/injector/domain.test.ts b/tests/injector/domain.test.ts similarity index 100% rename from test/injector/domain.test.ts rename to tests/injector/domain.test.ts diff --git a/test/injector/dynamicMultiple.test.ts b/tests/injector/dynamicMultiple.test.ts similarity index 100% rename from test/injector/dynamicMultiple.test.ts rename to tests/injector/dynamicMultiple.test.ts diff --git a/test/injector/hasInstance.test.ts b/tests/injector/hasInstance.test.ts similarity index 100% rename from test/injector/hasInstance.test.ts rename to tests/injector/hasInstance.test.ts diff --git a/test/injector/overrideProviders.test.ts b/tests/injector/overrideProviders.test.ts similarity index 100% rename from test/injector/overrideProviders.test.ts rename to tests/injector/overrideProviders.test.ts diff --git a/test/injector/strictMode.test.ts b/tests/injector/strictMode.test.ts similarity index 100% rename from test/injector/strictMode.test.ts rename to tests/injector/strictMode.test.ts diff --git a/test/injector/tag.test.ts b/tests/injector/tag.test.ts similarity index 100% rename from test/injector/tag.test.ts rename to tests/injector/tag.test.ts diff --git a/test/providers/useAlias.test.ts b/tests/providers/useAlias.test.ts similarity index 100% rename from test/providers/useAlias.test.ts rename to tests/providers/useAlias.test.ts diff --git a/test/use-case.test.ts b/tests/use-case.test.ts similarity index 100% rename from test/use-case.test.ts rename to tests/use-case.test.ts diff --git a/tsconfig.json b/tsconfig.json index 599fbdb..928cc64 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,5 +14,5 @@ "skipLibCheck": true, "moduleResolution": "node" }, - "include": ["src", "test"] + "include": ["src", "tests"] } From 072f5c093a7ceca10d3ea9f05dfdbd482aa74d6d Mon Sep 17 00:00:00 2001 From: artin Date: Thu, 14 Dec 2023 10:39:32 +0800 Subject: [PATCH 22/25] test: rename test name --- tests/helper/dep-helper.test.ts | 2 +- tests/helper/injector-helper.test.ts | 2 +- tests/helper/is-function.test.ts | 2 +- tests/helper/parameter-helper.test.ts | 2 +- tests/helper/provider-helper.test.ts | 2 +- tests/helper/reflect-helper.test.ts | 2 +- tests/helper/{util-helper.test.ts => utils-helper.test.ts} | 2 +- tests/use-case.test.ts | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) rename tests/helper/{util-helper.test.ts => utils-helper.test.ts} (94%) diff --git a/tests/helper/dep-helper.test.ts b/tests/helper/dep-helper.test.ts index 0098e72..bd0400e 100644 --- a/tests/helper/dep-helper.test.ts +++ b/tests/helper/dep-helper.test.ts @@ -1,7 +1,7 @@ import * as Helper from '../../src/helper/dep-helper'; import { ConstructorOf } from '../../src'; -describe(__filename, () => { +describe('dep helper', () => { let Parent: ConstructorOf; let Constructor: ConstructorOf; diff --git a/tests/helper/injector-helper.test.ts b/tests/helper/injector-helper.test.ts index 7b8dfa1..997cf63 100644 --- a/tests/helper/injector-helper.test.ts +++ b/tests/helper/injector-helper.test.ts @@ -4,7 +4,7 @@ import { ConstructorOf, InstanceOpts } from '../../src'; // eslint-disable-next-line const pkg = require('../../package.json'); -describe(__filename, () => { +describe('injector helper', () => { let Parent: ConstructorOf; let Constructor: ConstructorOf; diff --git a/tests/helper/is-function.test.ts b/tests/helper/is-function.test.ts index 7dfa78d..216dec3 100644 --- a/tests/helper/is-function.test.ts +++ b/tests/helper/is-function.test.ts @@ -9,7 +9,7 @@ import { CreatorStatus, } from '../../src'; -describe(__filename, () => { +describe('is function', () => { class A {} const clsToken: Token = A; const strToken: Token = 'strToken'; diff --git a/tests/helper/parameter-helper.test.ts b/tests/helper/parameter-helper.test.ts index d365e6f..857c8e1 100644 --- a/tests/helper/parameter-helper.test.ts +++ b/tests/helper/parameter-helper.test.ts @@ -1,7 +1,7 @@ import * as Helper from '../../src/helper/parameter-helper'; import { ConstructorOf } from '../../src'; -describe(__filename, () => { +describe('parameter helper', () => { let Parent: ConstructorOf; let Constructor: ConstructorOf; diff --git a/tests/helper/provider-helper.test.ts b/tests/helper/provider-helper.test.ts index 2269083..5405b60 100644 --- a/tests/helper/provider-helper.test.ts +++ b/tests/helper/provider-helper.test.ts @@ -9,7 +9,7 @@ import { Injectable, } from '../../src'; -describe(__filename, () => { +describe('provider helper', () => { @Injectable() class A {} const clsToken: Token = A; diff --git a/tests/helper/reflect-helper.test.ts b/tests/helper/reflect-helper.test.ts index 1678cd4..569487a 100644 --- a/tests/helper/reflect-helper.test.ts +++ b/tests/helper/reflect-helper.test.ts @@ -1,7 +1,7 @@ import * as Helper from '../../src/helper/reflect-helper'; import { ConstructorOf } from '../../src'; -describe(__filename, () => { +describe('reflect helper', () => { let Parent: ConstructorOf; let Constructor: ConstructorOf; diff --git a/tests/helper/util-helper.test.ts b/tests/helper/utils-helper.test.ts similarity index 94% rename from tests/helper/util-helper.test.ts rename to tests/helper/utils-helper.test.ts index c6476b9..da1de4d 100644 --- a/tests/helper/util-helper.test.ts +++ b/tests/helper/utils-helper.test.ts @@ -1,6 +1,6 @@ import * as Helper from '../../src/helper/utils'; -describe(__filename, () => { +describe('utils helper', () => { it('uniq', () => { const obj = {}; const arr = [1, 2, obj, obj, 3]; diff --git a/tests/use-case.test.ts b/tests/use-case.test.ts index 612b740..68e41a9 100644 --- a/tests/use-case.test.ts +++ b/tests/use-case.test.ts @@ -2,7 +2,7 @@ import { asSingleton, Autowired, Inject, Injectable, Injector } from '../src'; import * as Error from '../src/error'; -describe(__filename, () => { +describe('use cases', () => { it('使用 Autowired 动态注入依赖', () => { const spy = jest.fn(); @Injectable() From 68a8e830635b2154f4dd1babf87cf3e57b042de0 Mon Sep 17 00:00:00 2001 From: "lijiacheng.ljc" Date: Tue, 16 Jan 2024 11:01:43 +0800 Subject: [PATCH 23/25] ci(cov): skip unreachable code --- src/injector.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/injector.ts b/src/injector.ts index 4cb07ca..5f0f846 100644 --- a/src/injector.ts +++ b/src/injector.ts @@ -468,6 +468,7 @@ export class Injector { return creator.instances.values().next().value; } + /* istanbul ignore next */ throw noInstancesInCompletedCreatorError(ctx.token); } From 6bb85ec3d08f0abb6ecb63b137a0ecd3f02e1b61 Mon Sep 17 00:00:00 2001 From: "lijiacheng.ljc" Date: Tue, 16 Jan 2024 11:14:08 +0800 Subject: [PATCH 24/25] feat: add event emitter --- package.json | 3 - src/helper/event.ts | 57 ++++++++++++ src/helper/index.ts | 1 + src/injector.ts | 2 +- tests/helper/event.test.ts | 172 +++++++++++++++++++++++++++++++++++++ 5 files changed, 231 insertions(+), 4 deletions(-) create mode 100644 src/helper/event.ts create mode 100644 tests/helper/event.test.ts diff --git a/package.json b/package.json index a339c44..a97ab64 100644 --- a/package.json +++ b/package.json @@ -79,8 +79,5 @@ "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" - }, - "dependencies": { - "@opensumi/events": "^0.0.6" } } diff --git a/src/helper/event.ts b/src/helper/event.ts new file mode 100644 index 0000000..6051d9e --- /dev/null +++ b/src/helper/event.ts @@ -0,0 +1,57 @@ +/** + * Modified from https://github.com/opensumi/utils/blob/main/packages/events/src/index.ts + */ + +export type Handler = (...args: T) => void; + +export class EventEmitter> { + private _listeners: Map = new Map(); + + on(event: Event, listener: Handler) { + if (!this._listeners.has(event)) { + this._listeners.set(event, []); + } + this._listeners.get(event)!.push(listener); + + return () => this.off(event, listener); + } + + off(event: Event, listener: Handler) { + if (!this._listeners.has(event)) { + return; + } + const listeners = this._listeners.get(event)!; + const index = listeners.indexOf(listener); + if (index !== -1) { + listeners.splice(index, 1); + } + } + + once(event: Event, listener: Handler) { + const remove: () => void = this.on(event, (...args: Parameters>) => { + remove(); + listener.apply(this, args); + }); + + return remove; + } + + emit(event: Event, ...args: Parameters>) { + if (!this._listeners.has(event)) { + return; + } + [...this._listeners.get(event)!].forEach((listener) => listener.apply(this, args)); + } + + hasListener(event: Event) { + return this._listeners.has(event); + } + + getListeners(event: Event) { + return this._listeners.get(event) || []; + } + + dispose() { + this._listeners.clear(); + } +} diff --git a/src/helper/index.ts b/src/helper/index.ts index f3345e6..18d1b4a 100644 --- a/src/helper/index.ts +++ b/src/helper/index.ts @@ -5,3 +5,4 @@ export * from './provider-helper'; export * from './utils'; export * from './is-function'; export * from './hook-helper'; +export * from './event'; diff --git a/src/injector.ts b/src/injector.ts index 5f0f846..fa48828 100644 --- a/src/injector.ts +++ b/src/injector.ts @@ -49,8 +49,8 @@ import { isAspectCreator, getHookMeta, isAliasCreator, + EventEmitter, } from './helper'; -import { EventEmitter } from '@opensumi/events'; export class Injector { id = injectorIdGenerator.next(); diff --git a/tests/helper/event.test.ts b/tests/helper/event.test.ts new file mode 100644 index 0000000..892c36b --- /dev/null +++ b/tests/helper/event.test.ts @@ -0,0 +1,172 @@ +import { EventEmitter } from '../../src/helper/event'; + +describe('event emitter', () => { + it('basic usage', () => { + const emitter = new EventEmitter<{ + [key: string]: [string]; + }>(); + + const spy = jest.fn(); + const spy2 = jest.fn(); + emitter.on('test', spy); + emitter.on('foo', spy2); + + expect(emitter.hasListener('test')).toBe(true); + const listeners = emitter.getListeners('test'); + expect(listeners.length).toBe(1); + + emitter.emit('test', 'hello'); + expect(spy).toBeCalledWith('hello'); + emitter.off('test', spy); + + const listeners2 = emitter.getListeners('test'); + expect(listeners2.length).toBe(0); + + emitter.emit('test', 'hello'); + expect(spy).toBeCalledTimes(1); + + emitter.once('test', spy); + emitter.emit('test', 'hello'); + expect(spy).toBeCalledTimes(2); + emitter.emit('test', 'hello'); + expect(spy).toBeCalledTimes(2); + + emitter.off('bar', spy); + + emitter.dispose(); + + emitter.emit('test', 'hello'); + expect(spy).toBeCalledTimes(2); + }); + + it('many listeners listen to one event', () => { + const emitter = new EventEmitter<{ + [key: string]: [string]; + }>(); + const spy = jest.fn(); + const spy2 = jest.fn(); + emitter.on('test', spy); + emitter.on('test', spy2); + emitter.emit('test', 'hello'); + expect(spy).toBeCalledWith('hello'); + expect(spy2).toBeCalledWith('hello'); + + emitter.off('test', spy); + emitter.emit('test', 'hello'); + expect(spy).toBeCalledTimes(1); + expect(spy2).toBeCalledTimes(2); + + emitter.dispose(); + }); + + it('can dispose event listener by using returned function', () => { + const emitter = new EventEmitter<{ + [key: string]: [string]; + }>(); + const spy = jest.fn(); + const spy2 = jest.fn(); + const spy3 = jest.fn(); + const disposeSpy = emitter.on('test', spy); + emitter.on('test', spy2); + + const disposeSpy3 = emitter.once('test', spy3); + disposeSpy3(); + + emitter.emit('test', 'hello'); + expect(spy).toBeCalledWith('hello'); + expect(spy2).toBeCalledWith('hello'); + + disposeSpy(); + emitter.emit('test', 'hello'); + expect(spy).toBeCalledTimes(1); + expect(spy2).toBeCalledTimes(2); + expect(spy3).toBeCalledTimes(0); + emitter.dispose(); + }); +}); + +describe('event emitter types', () => { + it('basic usage', () => { + const emitter = new EventEmitter<{ + test: [string, string]; + foo: [string]; + }>(); + + const spy = jest.fn(); + const spy2 = jest.fn(); + + emitter.on('foo', spy2); + + expect(emitter.hasListener('test')).toBe(true); + const listeners = emitter.getListeners('test'); + expect(listeners.length).toBe(1); + + emitter.emit('test', 'hello', 'world'); + expect(spy).toBeCalledWith('hello'); + emitter.off('test', spy); + + const listeners2 = emitter.getListeners('test'); + expect(listeners2.length).toBe(0); + + emitter.emit('test', 'hello', 'world'); + expect(spy).toBeCalledTimes(1); + + emitter.once('test', spy); + emitter.emit('test', 'hello', 'world'); + expect(spy).toBeCalledTimes(2); + emitter.emit('test', 'hello', 'world'); + expect(spy).toBeCalledTimes(2); + + emitter.off('bar' as any, spy); + + emitter.dispose(); + + emitter.emit('test' as any, 'hello'); + expect(spy).toBeCalledTimes(2); + }); + + it('many listeners listen to one event', () => { + const emitter = new EventEmitter<{ + [key: string]: [string]; + }>(); + const spy = jest.fn(); + const spy2 = jest.fn(); + emitter.on('test', spy); + emitter.on('test', spy2); + emitter.emit('test', 'hello'); + expect(spy).toBeCalledWith('hello'); + expect(spy2).toBeCalledWith('hello'); + + emitter.off('test', spy); + emitter.emit('test', 'hello'); + expect(spy).toBeCalledTimes(1); + expect(spy2).toBeCalledTimes(2); + + emitter.dispose(); + }); + + it('can dispose event listener by using returned function', () => { + const emitter = new EventEmitter<{ + [key: string]: [string]; + }>(); + const spy = jest.fn(); + const spy2 = jest.fn(); + const spy3 = jest.fn(); + const disposeSpy = emitter.on('test', spy); + emitter.on('test', spy2); + + const disposeSpy3 = emitter.once('test', spy3); + disposeSpy3(); + + emitter.emit('test', 'hello'); + expect(spy).toBeCalledWith('hello'); + expect(spy2).toBeCalledWith('hello'); + + disposeSpy(); + emitter.emit('test', 'hello'); + expect(spy).toBeCalledTimes(1); + expect(spy2).toBeCalledTimes(2); + expect(spy3).toBeCalledTimes(0); + emitter.dispose(); + }); +}); From 48ea622180569e392a723e918b2b4992253ac4a5 Mon Sep 17 00:00:00 2001 From: "lijiacheng.ljc" Date: Tue, 16 Jan 2024 11:16:02 +0800 Subject: [PATCH 25/25] test: fix testcase --- tests/helper/event.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/helper/event.test.ts b/tests/helper/event.test.ts index 892c36b..56ba2c8 100644 --- a/tests/helper/event.test.ts +++ b/tests/helper/event.test.ts @@ -95,6 +95,7 @@ describe('event emitter types', () => { const spy = jest.fn(); const spy2 = jest.fn(); + emitter.on('test', spy); emitter.on('foo', spy2); expect(emitter.hasListener('test')).toBe(true); @@ -102,7 +103,7 @@ describe('event emitter types', () => { expect(listeners.length).toBe(1); emitter.emit('test', 'hello', 'world'); - expect(spy).toBeCalledWith('hello'); + expect(spy).toBeCalledWith('hello', 'world'); emitter.off('test', spy); const listeners2 = emitter.getListeners('test');