Skip to content

Commit

Permalink
feat: add events component (#4285)
Browse files Browse the repository at this point in the history
  • Loading branch information
czy88840616 authored Feb 4, 2025
1 parent e526f41 commit 4b59b47
Show file tree
Hide file tree
Showing 24 changed files with 2,583 additions and 1,714 deletions.
12 changes: 12 additions & 0 deletions .run/All Tests in event-emitter.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="All Tests in event-emitter" type="JavaScriptTestRunnerJest" folderName="midway all test">
<config-file value="$PROJECT_DIR$/packages/event-emitter/jest.config.js" />
<node-interpreter value="project" />
<node-options value="-r ts-node/register" />
<jest-package value="$PROJECT_DIR$/node_modules/jest" />
<working-dir value="$PROJECT_DIR$" />
<envs />
<scope-kind value="ALL" />
<method v="2" />
</configuration>
</component>
6 changes: 3 additions & 3 deletions packages/core/src/context/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,9 @@ export class MidwayContainer implements IMidwayGlobalContainer {
}

// inject constructor
const constructorProps = MetadataManager.getPropertiesWithMetadata<{
default: ConstructorInjectMetadata[];
}>(CONSTRUCTOR_INJECT_KEY, target);
const constructorProps = MetadataManager.getPropertiesWithMetadata<
ConstructorInjectMetadata[]
>(CONSTRUCTOR_INJECT_KEY, target);

if (constructorProps['default']) {
const argsLen = constructorProps['default'].length;
Expand Down
20 changes: 14 additions & 6 deletions packages/core/src/decorator/metadataManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,10 +325,12 @@ export class MetadataManager {
* @param target - The target class to retrieve properties with metadata from.
* @returns An object where the key is the property name and the value is the metadata value.
*/
public static getOwnPropertiesWithMetadata<T = Record<string, any>>(
public static getOwnPropertiesWithMetadata<T = any>(
metadataKey: string | symbol,
target: ClassType
): T {
): {
[key: string]: T;
} {
// Ensure the target is a class
target = this.formatTarget(target);

Expand All @@ -345,7 +347,9 @@ export class MetadataManager {
}
return result;
},
{} as T
{} as {
[key: string]: T;
}
);
}

Expand All @@ -356,11 +360,15 @@ export class MetadataManager {
* @param target - The target class to retrieve properties with metadata from.
* @returns An object where the key is the property name and the value is the metadata value.
*/
public static getPropertiesWithMetadata<T = Record<string, any>>(
public static getPropertiesWithMetadata<T = any>(
metadataKey: string | symbol,
target: ClassType
): T {
const propertiesWithMetadata = {} as T;
): {
[key: string]: T;
} {
const propertiesWithMetadata = {} as {
[key: string]: T;
};

// Traverse the prototype chain
let currentTarget: ClassType | null = this.formatTarget(target);
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/service/decoratorService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,9 @@ export class MidwayDecoratorService {

// find custom param decorator metadata
const parameterDecoratorMetadata =
MetadataManager.getOwnPropertiesWithMetadata<{
[methodName: string]: Array<ParameterDecoratorMetaData>;
}>(CUSTOM_PARAM_INJECT_KEY, Clzz);
MetadataManager.getOwnPropertiesWithMetadata<
Array<ParameterDecoratorMetaData>
>(CUSTOM_PARAM_INJECT_KEY, Clzz);

if (parameterDecoratorMetadata) {
debug(`[core]: Find parameter decorator metadata in ${Clzz.name}`);
Expand Down
12 changes: 12 additions & 0 deletions packages/event-emitter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# midwayjs etcd module

[![Package Quality](http://npm.packagequality.com/shield/midway-core.svg)](http://packagequality.com/#?package=midway-core)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/midwayjs/midway/pulls)

this is a sub package for midway.

Document: [https://midwayjs.org](https://midwayjs.org)

## License

[MIT]((https://github.com/midwayjs/midway/blob/master/LICENSE))
8 changes: 8 additions & 0 deletions packages/event-emitter/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { EventEmitterConfigOptions } from './dist/index';
export * from './dist/index';

declare module '@midwayjs/core/dist/interface' {
interface MidwayConfig {
eventEmitter?: Partial<EventEmitterConfigOptions>;
}
}
8 changes: 8 additions & 0 deletions packages/event-emitter/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testPathIgnorePatterns: ['<rootDir>/test/fixtures'],
coveragePathIgnorePatterns: ['<rootDir>/test/', '<rootDir>/dist/'],
setupFilesAfterEnv: ['./jest.setup.js'],
coverageProvider: 'v8',
};
2 changes: 2 additions & 0 deletions packages/event-emitter/jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
process.env.MIDWAY_TS_MODE = 'true';
jest.setTimeout(30000);
39 changes: 39 additions & 0 deletions packages/event-emitter/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "@midwayjs/event-emitter",
"description": "midway event emitter component",
"version": "4.0.0-alpha.1",
"main": "dist/index.js",
"typings": "index.d.ts",
"files": [
"dist/**/*.js",
"dist/**/*.d.ts",
"index.d.ts"
],
"devDependencies": {
"@midwayjs/core": "workspace:^",
"@midwayjs/mock": "workspace:^"
},
"dependencies": {
"eventemitter2": "6.4.9"
},
"keywords": [
"eventemitter",
"midway"
],
"author": "czy88840616 <[email protected]>",
"license": "MIT",
"scripts": {
"build": "tsc",
"test": "node -r ts-node/register ../../node_modules/jest/bin/jest.js --runInBand",
"cov": "node -r ts-node/register ../../node_modules/jest/bin/jest.js --runInBand --coverage --forceExit",
"ci": "npm run test",
"lint": "mwts check"
},
"engines": {
"node": ">=16"
},
"repository": {
"type": "git",
"url": "https://github.com/midwayjs/midway.git"
}
}
11 changes: 11 additions & 0 deletions packages/event-emitter/src/configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Configuration, IMidwayContainer } from '@midwayjs/core';
import { EventEmitterService } from './service';

@Configuration({
namespace: 'eventEmitter',
})
export class EventEmitterConfiguration {
async onReady(container: IMidwayContainer) {
await container.getAsync(EventEmitterService);
}
}
1 change: 1 addition & 0 deletions packages/event-emitter/src/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const EVENT_KEY = 'eventEmitter:on_event';
25 changes: 25 additions & 0 deletions packages/event-emitter/src/decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { EVENT_KEY } from './const';
import { OnEventOptions } from './interface';
import { DecoratorManager, MetadataManager } from '@midwayjs/core';

export function OnEvent(
event: string,
options?: OnEventOptions
): MethodDecorator {
return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
DecoratorManager.saveModule(EVENT_KEY, target.constructor);
MetadataManager.defineMetadata(
EVENT_KEY,
{
event,
options: {
...options,
suppressErrors: options?.suppressErrors ?? true,
},
},
target,
propertyKey
);
return descriptor;
};
}
4 changes: 4 additions & 0 deletions packages/event-emitter/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { EventEmitterConfiguration as Configuration } from './configuration';
export { EventEmitterService } from './service';
export * from './decorator';
export * from './interface';
21 changes: 21 additions & 0 deletions packages/event-emitter/src/interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ConstructorOptions, OnOptions } from 'eventemitter2';

export interface EventEmitterConfigOptions extends ConstructorOptions {}

export type OnEventOptions = OnOptions & {
/**
* If "true", prepends (instead of append) the given listener to the array of listeners.
*
* @see https://github.com/EventEmitter2/EventEmitter2#emitterprependlistenerevent-listener-options
*
* @default false
*/
prependListener?: boolean;

/**
* If "true", the onEvent callback will not throw an error while handling the event. Otherwise, if "false" it will throw an error.
*
* @default true
*/
suppressErrors?: boolean;
};
132 changes: 132 additions & 0 deletions packages/event-emitter/src/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { EventEmitter2 as EventEmitter, OnOptions } from 'eventemitter2';
import {
ApplicationContext,
Config,
DecoratorManager,
delegateTargetPrototypeMethod,
IMidwayContainer,
Init,
MetadataManager,
Singleton,
} from '@midwayjs/core';
import { EVENT_KEY } from './const';
import { EventEmitterConfigOptions, OnEventOptions } from './interface';

@Singleton()
export class EventEmitterService {
private eventEmitter: EventEmitter;

@Config('eventEmitter')
private config: EventEmitterConfigOptions;

@ApplicationContext()
private applicationContext: IMidwayContainer;

@Init()
async init() {
this.eventEmitter = new EventEmitter(this.config);
// proxy eventEmitter public methods to this
const methods = Object.getOwnPropertyNames(EventEmitter.prototype);
for (const method of methods) {
if (
method !== 'constructor' &&
typeof this.eventEmitter[method] === 'function'
) {
this[method] = this.eventEmitter[method].bind(this.eventEmitter);
}
}

const events = DecoratorManager.listModule(EVENT_KEY);

for (const eventModule of events) {
const props = MetadataManager.getPropertiesWithMetadata<{
event: string;
options: OnEventOptions;
}>(EVENT_KEY, eventModule);
for (const prop in props) {
const { event, options = {} } = props[prop];
const {
prependListener,
suppressErrors = true,
...emitterOptions
} = options;

const listener = async (...args) => {
try {
const instance = await this.applicationContext.getAsync(
eventModule
);
await instance[prop](...args);
} catch (err) {
if (!suppressErrors) {
throw err;
}
// 如果 suppressErrors 为 true,则忽略错误
}
};

if (prependListener) {
this.eventEmitter.prependListener(
event,
listener,
emitterOptions as OnOptions
);
} else {
this.eventEmitter.on(event, listener, emitterOptions as OnOptions);
}
}
}
}

getEventEmitter() {
return this.eventEmitter;
}

async emitAsync(
event: string | symbol | string[],
...args: any[]
): Promise<any[]> {
const listeners = this.eventEmitter.listeners(event as string);
const results = [];

// 执行所有监听器
for (const listener of listeners) {
try {
const result = await listener(...args);
if (result !== undefined) {
results.push(result);
}
} catch (err) {
// 如果错误不应该被抑制,则抛出
if (!err.suppressErrors) {
throw err;
}
}
}

return results;
}

emit(event: string | symbol | string[], ...args: any[]): boolean {
const result = this.eventEmitter.emit(event, ...args);
// 处理异步监听器
const listeners = this.eventEmitter.listeners(event as string);
Promise.all(
listeners.map(async listener => {
try {
if (listener.constructor.name === 'AsyncFunction') {
await listener(...args);
}
} catch (err) {
// 忽略同步 emit 中的错误
}
})
);
return result;
}
}

delegateTargetPrototypeMethod(EventEmitterService, [EventEmitter]);

// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface EventEmitterService extends EventEmitter {}
Loading

0 comments on commit 4b59b47

Please sign in to comment.