Skip to content

Commit

Permalink
fix: dispose should also delete its all instance cache
Browse files Browse the repository at this point in the history
  • Loading branch information
bytemain committed Oct 12, 2023
1 parent 07d3baf commit dae3ddd
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 26 deletions.
7 changes: 5 additions & 2 deletions src/decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
}
Expand All @@ -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);
};
}
Expand Down Expand Up @@ -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];
Expand Down
40 changes: 40 additions & 0 deletions src/helper/event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
export class EventEmitter<T> {
private _listeners: Map<T, Function[]> = new Map();

on(event: T, listener: Function) {
if (!this._listeners.has(event)) {
this._listeners.set(event, []);
}
this._listeners.get(event)!.push(listener);

Check warning on line 8 in src/helper/event.ts

View workflow job for this annotation

GitHub Actions / ci

Forbidden non-null assertion
}

off(event: T, listener: Function) {
if (!this._listeners.has(event)) {
return;
}
const listeners = this._listeners.get(event)!;

Check warning on line 15 in src/helper/event.ts

View workflow job for this annotation

GitHub Actions / ci

Forbidden non-null assertion
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));

Check warning on line 34 in src/helper/event.ts

View workflow job for this annotation

GitHub Actions / ci

Forbidden non-null assertion
}

dispose() {
this._listeners.clear();
}
}
2 changes: 1 addition & 1 deletion src/helper/hook-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ function isAfterThrowingHook<ThisType, Args extends any[], Result>(
return hook && hook.type === HookType.AfterThrowing;
}

function isPromiseLike(thing: any): thing is Promise<any> {
export function isPromiseLike(thing: any): thing is Promise<any> {
return !!(thing as Promise<any>).then;
}

Expand Down
44 changes: 21 additions & 23 deletions src/injector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
getHookMeta,
isAliasCreator,
} from './helper';
import { EventEmitter } from './helper/event';

export class Injector {
id = Helper.createId('Injector');
Expand Down Expand Up @@ -268,6 +269,12 @@ export class Injector {
return this.hookStore.createOneHook(hook);
}

private disposeEventEmitter = new EventEmitter<Token>();

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) {
Expand All @@ -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<any>();

const promises: Promise<unknown>[] = [];

// 还原对象状态
for (const creator of creatorMap.values()) {
const instance = creator.instance;
const promises: (Promise<unknown> | 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 {
Expand Down
29 changes: 29 additions & 0 deletions test/helper/event.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { EventEmitter } from '../../src/helper/event';

describe('event emitter', () => {
it('basic usage', () => {
const emitter = new EventEmitter<string>();
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);
});
});
65 changes: 65 additions & 0 deletions test/injector/dispose.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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);
});
});

0 comments on commit dae3ddd

Please sign in to comment.