diff --git a/src/decorator.ts b/src/decorator.ts index a48da19..376f0c6 100644 --- a/src/decorator.ts +++ b/src/decorator.ts @@ -56,7 +56,7 @@ interface InjectOpts { * @param token */ export function Inject(token: Token, opts: InjectOpts = {}): ParameterDecorator { - return (target, _: Token, index: number) => { + return (target, _: string | symbol | undefined, index: number) => { Helper.setParameterIn(target, { ...opts, token }, index); }; } @@ -66,7 +66,7 @@ export function Inject(token: Token, opts: InjectOpts = {}): ParameterDecorator * @param token */ export function Optional(token: Token = Symbol()): ParameterDecorator { - return (target, _: Token, index: number) => { + return (target, _: string | symbol | undefined, index: number) => { Helper.setParameterIn(target, { default: undefined, token }, index); }; } @@ -103,6 +103,9 @@ export function Autowired(token?: Token, opts?: InstanceOpts): PropertyDecorator } this[INSTANCE_KEY] = injector.get(realToken, opts); + injector.onceTokenDispose(realToken, () => { + this[INSTANCE_KEY] = undefined; + }); } return this[INSTANCE_KEY]; diff --git a/src/helper/event.ts b/src/helper/event.ts new file mode 100644 index 0000000..7854fcb --- /dev/null +++ b/src/helper/event.ts @@ -0,0 +1,40 @@ +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); + } + + 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 onceListener = (...args: any[]) => { + listener(...args); + this.off(event, onceListener); + }; + this.on(event, onceListener); + } + + emit(event: T, ...args: any[]) { + if (!this._listeners.has(event)) { + return; + } + this._listeners.get(event)!.forEach((listener) => listener(...args)); + } + + dispose() { + this._listeners.clear(); + } +} diff --git a/src/helper/hook-helper.ts b/src/helper/hook-helper.ts index 5693cd9..dd13baa 100644 --- a/src/helper/hook-helper.ts +++ b/src/helper/hook-helper.ts @@ -368,7 +368,7 @@ function isAfterThrowingHook( return hook && hook.type === HookType.AfterThrowing; } -function isPromiseLike(thing: any): thing is Promise { +export function isPromiseLike(thing: any): thing is Promise { return !!(thing as Promise).then; } diff --git a/src/injector.ts b/src/injector.ts index eae4ad2..54278fb 100644 --- a/src/injector.ts +++ b/src/injector.ts @@ -30,6 +30,7 @@ import { getHookMeta, isAliasCreator, } from './helper'; +import { EventEmitter } from './helper/event'; export class Injector { id = Helper.createId('Injector'); @@ -268,6 +269,12 @@ export class Injector { return this.hookStore.createOneHook(hook); } + private disposeEventEmitter = new EventEmitter(); + + onceTokenDispose(key: Token, cb: () => void) { + return this.disposeEventEmitter.once(key, cb); + } + disposeOne(token: Token, key = 'dispose') { const creator = this.creatorMap.get(token); if (!creator || creator.status === CreatorStatus.init) { @@ -282,38 +289,29 @@ export class Injector { creator.instance = undefined; creator.status = CreatorStatus.init; + + if (maybePromise && Helper.isPromiseLike(maybePromise)) { + maybePromise = maybePromise.then(() => { + this.disposeEventEmitter.emit(token); + }); + } else { + this.disposeEventEmitter.emit(token); + } return maybePromise; } disposeAll(key = 'dispose') { const creatorMap = this.creatorMap; - const toDisposeInstances = new Set(); - - const promises: Promise[] = []; - // 还原对象状态 - for (const creator of creatorMap.values()) { - const instance = creator.instance; + const promises: (Promise | undefined)[] = []; - if (creator.status === CreatorStatus.done) { - if (instance && typeof instance[key] === 'function') { - toDisposeInstances.add(instance); - } - - creator.instance = undefined; - creator.status = CreatorStatus.init; - } + for (const token of creatorMap.keys()) { + promises.push(this.disposeOne(token, key)); } - // 执行销毁函数 - for (const instance of toDisposeInstances) { - const maybePromise = instance[key](); - if (maybePromise) { - promises.push(maybePromise); - } - } - - return Promise.all(promises); + return Promise.all(promises).then(() => { + this.disposeEventEmitter.dispose(); + }); } protected getTagToken(token: Token, tag: Tag): Token | undefined | null { diff --git a/test/helper/event.test.ts b/test/helper/event.test.ts new file mode 100644 index 0000000..b118341 --- /dev/null +++ b/test/helper/event.test.ts @@ -0,0 +1,29 @@ +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); + }); +}); diff --git a/test/injector/dispose.test.ts b/test/injector/dispose.test.ts index 40c552f..8d094a5 100644 --- a/test/injector/dispose.test.ts +++ b/test/injector/dispose.test.ts @@ -111,6 +111,37 @@ describe('dispose', () => { injector.disposeOne(DisposeCls); expect(spy).toBeCalledTimes(1); }); + + it("dispose an instance will also dispose it's instance", () => { + const spy = jest.fn(); + + @Injectable() + class A { + constructor() { + spy(); + } + } + + @Injectable() + class B { + @Autowired() + a!: A; + } + + const instance = injector.get(B); + expect(injector.hasInstance(instance)).toBeTruthy(); + expect(instance).toBeInstanceOf(B); + expect(instance.a).toBeInstanceOf(A); + expect(spy).toBeCalledTimes(1); + + injector.disposeOne(A); + const creatorA = injector.creatorMap.get(A); + expect(creatorA!.status).toBe(CreatorStatus.init); + expect(creatorA!.instance).toBeUndefined(); + + expect(instance.a).toBeInstanceOf(A); + expect(spy).toBeCalledTimes(2); + }); }); describe('dispose asynchronous', () => { @@ -218,4 +249,38 @@ describe('dispose asynchronous', () => { await injector.disposeOne(DisposeCls); expect(spy).toBeCalledTimes(1); }); + + it("dispose an instance will also dispose it's instance", async () => { + const spy = jest.fn(); + + @Injectable() + class A { + constructor() { + spy(); + } + async dispose() { + await new Promise((resolve) => setTimeout(resolve, 100)); + } + } + + @Injectable() + class B { + @Autowired() + a!: A; + } + + const instance = injector.get(B); + expect(injector.hasInstance(instance)).toBeTruthy(); + expect(instance).toBeInstanceOf(B); + expect(instance.a).toBeInstanceOf(A); + expect(spy).toBeCalledTimes(1); + + await injector.disposeOne(A); + const creatorA = injector.creatorMap.get(A); + expect(creatorA!.status).toBe(CreatorStatus.init); + expect(creatorA!.instance).toBeUndefined(); + + expect(instance.a).toBeInstanceOf(A); + expect(spy).toBeCalledTimes(2); + }); });