Skip to content

Commit

Permalink
feat: allow a handler to subscribe to multiple events (#179)
Browse files Browse the repository at this point in the history
<!--
Thank you for your pull request. Please review below requirements.
Bug fixes and new features should include tests and possibly benchmarks.
Contributors guide:
https://github.com/eggjs/egg/blob/master/CONTRIBUTING.md

感谢您贡献代码。请确认下列 checklist 的完成情况。
Bug 修复和新功能必须包含测试,必要时请附上性能测试。
Contributors guide:
https://github.com/eggjs/egg/blob/master/CONTRIBUTING.md
-->

##### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to
[x]. -->

- [x] `npm test` passes
- [x] tests and/or benchmarks are included
- [x] documentation is changed or added
- [x] commit message follows commit guidelines

##### Affected core subsystem(s)
<!-- Provide affected core subsystem(s). -->
`plugin/eventbus`  `eventbus-decorator`  `eventbus-runtime` 

##### Description of change
<!-- Provide a description of the change below this comment. -->
allow a handler to subscribe to multiple events
<!--
- any feature?
- close https://github.com/eggjs/egg/ISSUE_URL
-->
  • Loading branch information
YdreamW authored Dec 26, 2023
1 parent 04fabef commit 1d460a5
Show file tree
Hide file tree
Showing 19 changed files with 356 additions and 32 deletions.
26 changes: 26 additions & 0 deletions core/eventbus-decorator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,29 @@ export class Foo {
}
}
```

### handle multiple event
```ts
@Event('hello')
@Event('hi')
export class Foo {
async handle(msg: string): Promise<void> {
console.log('msg: ', msg);
}
}
```

### inject event context
inject event context if you want to know which event is being handled.
The context param must be the first param

```ts
@Event('hello')
@Event('hi')
export class Foo {
async handle(@EventContext() ctx: IEventContext, msg: string):Promise<void> {
console.log('eventName: ', ctx.eventName);
console.log('msg: ', msg);
}
}
```
1 change: 1 addition & 0 deletions core/eventbus-decorator/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './src/EventBus';
export * from './src/Event';
export * from './src/EventInfoUtil';
export * from './src/EventContext';

// trick for use declaration
export interface Events {
Expand Down
4 changes: 2 additions & 2 deletions core/eventbus-decorator/src/Event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import { EventHandler } from '../index';
import { EventInfoUtil } from './EventInfoUtil';
// use @eggjs/tegg as namespace
// eslint-disable-next-line import/no-unresolved
import { Events } from '@eggjs/tegg';
import type { Events } from '@eggjs/tegg';

export function Event<E extends keyof Events>(eventName: E) {
return function(clazz: new () => EventHandler<E>) {
EventInfoUtil.setEventName(eventName, clazz);
EventInfoUtil.addEventName(eventName, clazz);
const func = SingletonProto({
accessLevel: AccessLevel.PUBLIC,
});
Expand Down
15 changes: 11 additions & 4 deletions core/eventbus-decorator/src/EventBus.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import TypedEventEmitter, { Arguments } from 'typed-emitter';
import TypedEventEmitter from 'typed-emitter';
import type { Arguments } from 'typed-emitter';
// use @eggjs/tegg as namespace
// eslint-disable-next-line import/no-unresolved
import { Events } from '@eggjs/tegg';
import type { Events } from '@eggjs/tegg';
import { IEventContext } from './EventContext';

export type EventName = string | symbol;
export type { Arguments } from 'typed-emitter';

/**
* use `emit` to emit a event
Expand Down Expand Up @@ -38,6 +41,10 @@ export interface EventWaiter {
awaitFirst<E1 extends EventKeys, E2 extends EventKeys, E3 extends EventKeys, E4 extends EventKeys>(e1: E1, e2: E2, e3: E3, e4: E4): Promise<{ event: E1 | E2 | E3 | E4, args: Arguments<Events[E1] | Events[E2] | Events[E3] | Events[E4]> }>
}

export interface EventHandler<E extends keyof Events> {
type EventHandlerWithContext<E extends keyof Events> = {
handle: (ctx: IEventContext, ...args: Arguments<Events[E]>) => ReturnType<Events[E]>
};

export type EventHandler<E extends keyof Events> = {
handle: Events[E];
}
} | EventHandlerWithContext<E>;
21 changes: 21 additions & 0 deletions core/eventbus-decorator/src/EventContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// use @eggjs/tegg as namespace
// eslint-disable-next-line import/no-unresolved
import type { Events } from '@eggjs/tegg';
import { EggProtoImplClass } from '@eggjs/core-decorator';
import assert from 'assert';
import { EventInfoUtil } from './EventInfoUtil';

export interface IEventContext{
eventName: keyof Events
}

export function EventContext() {
return function(target: any, propertyKey: PropertyKey, parameterIndex: number) {
assert(propertyKey === 'handle',
`[eventHandler ${target.name}] expect method name be handle, but now is ${String(propertyKey)}`);
assert(parameterIndex === 0,
`[eventHandler ${target.name}] expect param EventContext be the first param`);
const clazz = target.constructor as EggProtoImplClass;
EventInfoUtil.setEventHandlerContextInject(true, clazz);
};
}
33 changes: 31 additions & 2 deletions core/eventbus-decorator/src/EventInfoUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,42 @@ import { EggProtoImplClass, MetadataUtil } from '@eggjs/core-decorator';
import { EventName } from './EventBus';

export const EVENT_NAME = Symbol.for('EggPrototype#eventName');
export const EVENT_CONTEXT_INJECT = Symbol.for('EggPrototype#event#handler#context#inject');

export class EventInfoUtil {
/**
* @deprecated
*/
static setEventName(eventName: EventName, clazz: EggProtoImplClass) {
MetadataUtil.defineMetaData(EVENT_NAME, eventName, clazz);
EventInfoUtil.addEventName(eventName, clazz);
}

static addEventName(eventName: EventName, clazz: EggProtoImplClass) {
const eventNameList = MetadataUtil.initOwnArrayMetaData<EventName>(EVENT_NAME, clazz, []);
eventNameList.push(eventName);
}

static getEventNameList(clazz: EggProtoImplClass): EventName[] {
return MetadataUtil.getArrayMetaData(EVENT_NAME, clazz);
}

/**
* @deprecated
* return the last eventName which is subscribed firstly by Event decorator
*/
static getEventName(clazz: EggProtoImplClass): EventName | undefined {
return MetadataUtil.getMetaData(EVENT_NAME, clazz);
const eventNameList = MetadataUtil.initOwnArrayMetaData<EventName>(EVENT_NAME, clazz, []);
if (eventNameList.length !== 0) {
return eventNameList[eventNameList.length - 1];
}
return undefined;
}

static setEventHandlerContextInject(enable: boolean, clazz: EggProtoImplClass): void {
MetadataUtil.defineMetaData(EVENT_CONTEXT_INJECT, enable, clazz);
}

static getEventHandlerContextInject(clazz: EggProtoImplClass): boolean {
return MetadataUtil.getMetaData(EVENT_CONTEXT_INJECT, clazz) ?? false;
}
}
28 changes: 25 additions & 3 deletions core/eventbus-decorator/test/Event.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,34 @@ import assert from 'assert';
import path from 'path';
import coffee from 'coffee';
import { FooHandler } from './fixtures/right-event-handle';
import { MultiHandler } from './fixtures/multiple-events-handle';
import { EventContextHandler } from './fixtures/event-handle-with-context';
import { EmptyHandler } from './fixtures/empty-handle';
import { EventInfoUtil } from '../src/EventInfoUtil';

describe('test/Event.test.ts', () => {
it('should work', () => {
const eventName = EventInfoUtil.getEventName(FooHandler);
assert(eventName === 'foo');
it('getEventName should work', () => {
assert.equal(EventInfoUtil.getEventName(FooHandler), 'foo');
assert.equal(EventInfoUtil.getEventName(EmptyHandler), undefined);
});

it('getEventNameList should work', function() {
const event = EventInfoUtil.getEventName(MultiHandler);
assert.deepStrictEqual(event, 'hello');
const eventList = EventInfoUtil.getEventNameList(MultiHandler);
assert.deepStrictEqual(eventList, [ 'hi', 'hello' ]);
});

it('setEventName should work', function() {
EventInfoUtil.setEventName('foo', EmptyHandler);
assert.equal(EventInfoUtil.getEventName(EmptyHandler), 'foo');
EventInfoUtil.setEventName('bar', EmptyHandler);
assert.equal(EventInfoUtil.getEventName(EmptyHandler), 'bar');
});

it('getEventHandlerContextInject', function() {
assert.equal(EventInfoUtil.getEventHandlerContextInject(EventContextHandler), true);
assert.equal(EventInfoUtil.getEventHandlerContextInject(FooHandler), false);
});

it('event type check should work', async () => {
Expand Down
1 change: 1 addition & 0 deletions core/eventbus-decorator/test/fixtures/empty-handle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class EmptyHandler{}
28 changes: 28 additions & 0 deletions core/eventbus-decorator/test/fixtures/event-handle-with-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Inject, SingletonProto } from '@eggjs/core-decorator';
import {EventBus, Event, IEventContext, EventContext} from '../..';


declare module '@eggjs/tegg' {
interface Events {
ctxEvent: (msg: string) => void;
}
}

@SingletonProto()
export class EventContextProducer {
@Inject()
private readonly eventBus: EventBus;

trigger() {
this.eventBus.emit('ctxEvent', 'hello');
}
}

@Event('ctxEvent')
export class EventContextHandler {
handle(@EventContext() ctx: IEventContext, msg: string): void {
console.log('ctx: ', ctx);
console.log('msg: ', msg);
}
}

29 changes: 29 additions & 0 deletions core/eventbus-decorator/test/fixtures/multiple-events-handle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Inject, SingletonProto } from '@eggjs/core-decorator';
import { EventBus, Event } from '../..';


declare module '@eggjs/tegg' {
interface Events {
hello: (msg: string) => void;
hi: (msg: string) => void;
}
}

@SingletonProto()
export class MultiProducer {
@Inject()
private readonly eventBus: EventBus;

trigger() {
this.eventBus.emit('hello', 'Ydream');
}
}

@Event('hello')
@Event('hi')
export class MultiHandler {
handle(msg: string): void {
console.log('msg: ', msg);
}
}

32 changes: 27 additions & 5 deletions core/eventbus-runtime/src/EventHandlerFactory.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EventHandler, EventName, Events } from '@eggjs/eventbus-decorator';
import { EventHandler, EventName, Events, Arguments, EVENT_CONTEXT_INJECT } from '@eggjs/eventbus-decorator';
import { EggContainerFactory } from '@eggjs/tegg-runtime';
import { EggPrototype } from '@eggjs/tegg-metadata';
import { MapUtil } from '@eggjs/tegg-common-util';
Expand All @@ -19,11 +19,33 @@ export class EventHandlerFactory {
return this.handlerProtoMap.has(event);
}

async getHandlers(event: EventName): Promise<Array<EventHandler<keyof Events>>> {
getHandlerProtos(event: EventName): Array<EggPrototype> {
const handlerProtos = this.handlerProtoMap.get(event) || [];
const eggObjs = await Promise.all(handlerProtos.map(proto => {
return EggContainerFactory.getOrCreateEggObject(proto, proto.name);
return handlerProtos;
}

async getHandler(proto: EggPrototype): Promise<EventHandler<keyof Events>> {
const eggObj = await EggContainerFactory.getOrCreateEggObject(proto, proto.name);
return eggObj.obj as EventHandler<keyof Events>;
}

async getHandlers(event: EventName): Promise<Array<EventHandler<keyof Events>>> {
const handlerProtos = this.getHandlerProtos(event);
return await Promise.all(handlerProtos.map(proto => {
return this.getHandler(proto);
}));
return eggObjs.map(t => t.obj as EventHandler<keyof Events>);
}

async handle(eventName: EventName, proto: EggPrototype, args: Arguments<any>): Promise<void> {
const handler = await this.getHandler(proto);
const enableInjectCtx = proto.getMetaData<boolean>(EVENT_CONTEXT_INJECT) ?? false;
if (enableInjectCtx) {
const ctx = {
eventName,
};
await Reflect.apply(handler.handle, handler, [ ctx, ...args ]);
} else {
await Reflect.apply(handler.handle, handler, args);
}
}
}
22 changes: 9 additions & 13 deletions core/eventbus-runtime/src/SingletonEventBus.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AccessLevel, Inject, SingletonProto } from '@eggjs/core-decorator';
import { EventBus, Events, EventWaiter, EventName, CORK_ID } from '@eggjs/eventbus-decorator';
import type { Arguments } from '@eggjs/eventbus-decorator';
import { ContextHandler, EggContext } from '@eggjs/tegg-runtime';
import type { EggLogger } from 'egg';
import { EventContextFactory } from './EventContextFactory';
Expand All @@ -8,11 +9,6 @@ import { EventEmitter } from 'events';
import awaitEvent from 'await-event';
import awaitFirst from 'await-first';

// from typed-emitter
type Array<T> = [ T ] extends [ (...args: infer U) => any ]
? U
: [ T ] extends [ void ] ? [] : [ T ];

export interface Event {
name: EventName;
args: Array<any>;
Expand Down Expand Up @@ -56,15 +52,15 @@ export class SingletonEventBus implements EventBus, EventWaiter {
return this;
}

async await<E extends keyof Events>(event: E): Promise<Array<Events[E]>> {
async await<E extends keyof Events>(event: E): Promise<Arguments<Events[E]>> {
return awaitEvent(this.emitter, event);
}

awaitFirst<E extends keyof Events>(...e: Array<E>): Promise<{ event: EventName, args: Array<Events[E]> }> {
awaitFirst<E extends keyof Events>(...e: Array<E>): Promise<{ event: EventName, args: Arguments<Events[E]> }> {
return awaitFirst(this.emitter, e);
}

emit<E extends keyof Events>(event: E, ...args: Array<Events[E]>): boolean {
emit<E extends keyof Events>(event: E, ...args: Arguments<Events[E]>): boolean {
const ctx = this.eventContextFactory.createContext();
const hasListener = this.eventHandlerFactory.hasListeners(event);
this.doEmit(ctx, event, args);
Expand Down Expand Up @@ -112,7 +108,7 @@ export class SingletonEventBus implements EventBus, EventWaiter {
corkdEvents.events.push(event);
}

emitWithContext<E extends keyof Events>(parentContext: EggContext, event: E, args: Array<Events[E]>): boolean {
emitWithContext<E extends keyof Events>(parentContext: EggContext, event: E, args: Arguments<Events[E]>): boolean {
const corkId = parentContext.get(CORK_ID);
const hasListener = this.eventHandlerFactory.hasListeners(event);
if (corkId) {
Expand Down Expand Up @@ -145,13 +141,13 @@ export class SingletonEventBus implements EventBus, EventWaiter {
await ctx.init(lifecycle);
}
try {
const handlers = await this.eventHandlerFactory.getHandlers(event);
await Promise.all(handlers.map(async handler => {
const handlerProtos = this.eventHandlerFactory.getHandlerProtos(event);
await Promise.all(handlerProtos.map(async proto => {
try {
await Reflect.apply(handler.handle, handler, args);
await this.eventHandlerFactory.handle(event, proto, args);
} catch (e) {
// should wait all handlers done then destroy ctx
e.message = `[EventBus] process event ${String(event)} failed: ${e.message}`;
e.message = `[EventBus] process event ${String(event)} for handler ${String(proto.name)} failed: ${e.message}`;
this.logger.error(e);
}
}));
Expand Down
Loading

0 comments on commit 1d460a5

Please sign in to comment.