Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eventbus): allow a handler to subscribe to multiple events #179

Merged
merged 4 commits into from
Dec 26, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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> {
killagu marked this conversation as resolved.
Show resolved Hide resolved
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
2 changes: 1 addition & 1 deletion core/eventbus-decorator/src/Event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { 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
13 changes: 10 additions & 3 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 { 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 { Events } from '@eggjs/tegg';
killagu marked this conversation as resolved.
Show resolved Hide resolved
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) {
killagu marked this conversation as resolved.
Show resolved Hide resolved
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 {
killagu marked this conversation as resolved.
Show resolved Hide resolved
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
Loading