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 2 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> {
killagu marked this conversation as resolved.
Show resolved Hide resolved
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 { 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);

Check warning on line 12 in core/eventbus-decorator/src/EventInfoUtil.ts

View check run for this annotation

Codecov / codecov/patch

core/eventbus-decorator/src/EventInfoUtil.ts#L12

Added line #L12 was not covered by tests
}

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;

Check warning on line 33 in core/eventbus-decorator/src/EventInfoUtil.ts

View check run for this annotation

Codecov / codecov/patch

core/eventbus-decorator/src/EventInfoUtil.ts#L33

Added line #L33 was not covered by tests
}

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;
}
}
20 changes: 17 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,26 @@ 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 { EventInfoUtil } from '../src/EventInfoUtil';

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

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('getEventHandlerContextInject', function() {
assert.equal(EventInfoUtil.getEventHandlerContextInject(EventContextHandler), true);
assert.equal(EventInfoUtil.getEventHandlerContextInject(FooHandler), false);
});

it('event type check should work', async () => {
Expand Down
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 @@
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);

Check warning on line 35 in core/eventbus-runtime/src/EventHandlerFactory.ts

View check run for this annotation

Codecov / codecov/patch

core/eventbus-runtime/src/EventHandlerFactory.ts#L32-L35

Added lines #L32 - L35 were not covered by tests
}));
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 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 @@
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]> }> {

Check warning on line 59 in core/eventbus-runtime/src/SingletonEventBus.ts

View check run for this annotation

Codecov / codecov/patch

core/eventbus-runtime/src/SingletonEventBus.ts#L59

Added line #L59 was not covered by tests
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 @@
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 @@
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
27 changes: 27 additions & 0 deletions core/eventbus-runtime/test/EventBus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { EventInfoUtil, CORK_ID } from '@eggjs/eventbus-decorator';
import { CoreTestHelper, EggTestContext } from '@eggjs/module-test-util';
import { EventContextFactory, EventHandlerFactory, SingletonEventBus } from '..';
import { Timeout0Handler, Timeout100Handler, TimeoutProducer } from './fixtures/modules/event/MultiEvent';
import { MultiWithContextHandler, MultiWithContextProducer } from './fixtures/modules/event/MultiEventWithContext';

describe('test/EventBus.test.ts', () => {
let modules: Array<LoadUnitInstance>;
Expand Down Expand Up @@ -54,6 +55,32 @@ describe('test/EventBus.test.ts', () => {
});
});

it('should work with EventContext', async function() {
await EggTestContext.mockContext(async (ctx: EggTestContext) => {
const eventContextFactory = await CoreTestHelper.getObject(EventContextFactory);
eventContextFactory.registerContextCreator(() => {
return ctx;
});
const eventHandlerFactory = await CoreTestHelper.getObject(EventHandlerFactory);
EventInfoUtil.getEventNameList(MultiWithContextHandler)
.forEach(eventName =>
eventHandlerFactory.registerHandler(eventName, PrototypeUtil.getClazzProto(MultiWithContextHandler) as EggPrototype));

const eventBus = await CoreTestHelper.getObject(SingletonEventBus);
const producer = await CoreTestHelper.getObject(MultiWithContextProducer);
const fooEvent = eventBus.await('foo');
producer.foo();
await fooEvent;
assert.equal(MultiWithContextHandler.eventName, 'foo');
assert.equal(MultiWithContextHandler.msg, '123');
const barEvent = eventBus.await('bar');
producer.bar();
await barEvent;
assert.equal(MultiWithContextHandler.eventName, 'bar');
assert.equal(MultiWithContextHandler.msg, '321');
});
});

it('destroy should be called', async () => {
await EggTestContext.mockContext(async (ctx: EggTestContext) => {
let destroyCalled = false;
Expand Down
Loading
Loading