mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
feat(server): job system (#10134)
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
import { OnOptions } from 'eventemitter2';
|
||||
|
||||
import { PushMetadata, sliceMetadata } from '../nestjs';
|
||||
|
||||
declare global {
|
||||
/**
|
||||
* Event definitions can be extended by
|
||||
@@ -16,3 +20,29 @@ declare global {
|
||||
}
|
||||
|
||||
export type EventName = keyof Events;
|
||||
export const EVENT_LISTENER_METADATA = Symbol('event_listener');
|
||||
|
||||
interface EventHandlerMetadata {
|
||||
namespace: string;
|
||||
event: EventName;
|
||||
opts?: OnOptions;
|
||||
}
|
||||
|
||||
export interface EventOptions extends OnOptions {
|
||||
prepend?: boolean;
|
||||
name?: string;
|
||||
suppressError?: boolean;
|
||||
}
|
||||
|
||||
export const OnEvent = (event: EventName, opts?: EventOptions) => {
|
||||
const namespace = event.split('.')[0];
|
||||
return PushMetadata<EventHandlerMetadata>(EVENT_LISTENER_METADATA, {
|
||||
namespace,
|
||||
event,
|
||||
opts,
|
||||
});
|
||||
};
|
||||
|
||||
export function getEventHandlerMetadata(target: any): EventHandlerMetadata[] {
|
||||
return sliceMetadata<EventHandlerMetadata>(EVENT_LISTENER_METADATA, target);
|
||||
}
|
||||
|
||||
@@ -4,42 +4,20 @@ import {
|
||||
OnApplicationBootstrap,
|
||||
OnModuleInit,
|
||||
} from '@nestjs/common';
|
||||
import { DiscoveryService, MetadataScanner } from '@nestjs/core';
|
||||
import {
|
||||
OnGatewayConnection,
|
||||
WebSocketGateway,
|
||||
WebSocketServer,
|
||||
} from '@nestjs/websockets';
|
||||
import EventEmitter2, { type OnOptions } from 'eventemitter2';
|
||||
import EventEmitter2 from 'eventemitter2';
|
||||
import { once } from 'lodash-es';
|
||||
import { CLS_ID, ClsService, ClsServiceManager } from 'nestjs-cls';
|
||||
import type { Server, Socket } from 'socket.io';
|
||||
|
||||
import { wrapCallMetric } from '../metrics';
|
||||
import { PushMetadata, sliceMetadata } from '../nestjs';
|
||||
import { genRequestId } from '../utils';
|
||||
import type { EventName } from './def';
|
||||
|
||||
const EVENT_LISTENER_METADATA = Symbol('event_listener');
|
||||
interface EventHandlerMetadata {
|
||||
namespace: string;
|
||||
event: EventName;
|
||||
opts?: OnOptions;
|
||||
}
|
||||
|
||||
interface EventOptions extends OnOptions {
|
||||
prepend?: boolean;
|
||||
name?: string;
|
||||
suppressError?: boolean;
|
||||
}
|
||||
|
||||
export const OnEvent = (event: EventName, opts?: EventOptions) => {
|
||||
const namespace = event.split('.')[0];
|
||||
return PushMetadata<EventHandlerMetadata>(EVENT_LISTENER_METADATA, {
|
||||
namespace,
|
||||
event,
|
||||
opts,
|
||||
});
|
||||
};
|
||||
import { type EventName, type EventOptions } from './def';
|
||||
import { EventHandlerScanner } from './scanner';
|
||||
|
||||
/**
|
||||
* We use socket.io system to auto pub/sub on server to server broadcast events
|
||||
@@ -59,8 +37,7 @@ export class EventBus
|
||||
constructor(
|
||||
private readonly emitter: EventEmitter2,
|
||||
private readonly cls: ClsService,
|
||||
private readonly discovery: DiscoveryService,
|
||||
private readonly scanner: MetadataScanner
|
||||
private readonly scanner: EventHandlerScanner
|
||||
) {}
|
||||
|
||||
handleConnection(client: Socket) {
|
||||
@@ -119,13 +96,14 @@ export class EventBus
|
||||
) {
|
||||
const namespace = event.split('.')[0];
|
||||
const { name, prepend, suppressError } = opts;
|
||||
let signature = name ?? listener.name ?? 'anonymous fn';
|
||||
const handlerName = name ?? listener.name ?? 'anonymous fn';
|
||||
let signature = `[${event}] (${handlerName})`;
|
||||
|
||||
const add = prepend ? this.emitter.prependListener : this.emitter.on;
|
||||
|
||||
const handler = wrapCallMetric(
|
||||
async (payload: any) => {
|
||||
this.logger.verbose(`Handle event [${event}] (${signature})`);
|
||||
this.logger.verbose(`Handle event ${signature}`);
|
||||
|
||||
const cls = ClsServiceManager.getClsService();
|
||||
return await cls.run({ ifNested: 'reuse' }, async () => {
|
||||
@@ -138,7 +116,7 @@ export class EventBus
|
||||
} catch (e) {
|
||||
if (suppressError) {
|
||||
this.logger.error(
|
||||
`Error happened when handling event [${event}] (${signature})`,
|
||||
`Error happened when handling event ${signature}`,
|
||||
e
|
||||
);
|
||||
} else {
|
||||
@@ -152,15 +130,13 @@ export class EventBus
|
||||
{
|
||||
event,
|
||||
namespace,
|
||||
handler: signature,
|
||||
handler: handlerName,
|
||||
}
|
||||
);
|
||||
|
||||
add.call(this.emitter, event, handler as any, opts);
|
||||
|
||||
this.logger.verbose(
|
||||
`Event handler for [${event}] registered ${name ? `in [${name}]` : ''}`
|
||||
);
|
||||
this.logger.verbose(`Event handler registered ${signature}`);
|
||||
|
||||
return () => {
|
||||
this.emitter.off(event, handler as any);
|
||||
@@ -171,60 +147,9 @@ export class EventBus
|
||||
return this.emitter.waitFor(name, timeout);
|
||||
}
|
||||
|
||||
private bindEventHandlers() {
|
||||
// make sure all our job handlers defined in [Providers] to make the code organization clean.
|
||||
// const providers = [...this.discovery.getProviders(), this.discovery.getControllers()]
|
||||
const providers = this.discovery.getProviders();
|
||||
|
||||
providers.forEach(wrapper => {
|
||||
const { instance, name } = wrapper;
|
||||
if (!instance || wrapper.isAlias) {
|
||||
return;
|
||||
}
|
||||
|
||||
const proto = Object.getPrototypeOf(instance);
|
||||
const methods = this.scanner.getAllMethodNames(proto);
|
||||
|
||||
methods.forEach(method => {
|
||||
const fn = instance[method];
|
||||
|
||||
let defs = sliceMetadata<EventHandlerMetadata>(
|
||||
EVENT_LISTENER_METADATA,
|
||||
fn
|
||||
);
|
||||
|
||||
if (defs.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const signature = `${name}.${method}`;
|
||||
|
||||
if (typeof fn !== 'function') {
|
||||
throw new Error(`Event handler [${signature}] is not a function.`);
|
||||
}
|
||||
|
||||
if (!wrapper.isDependencyTreeStatic()) {
|
||||
throw new Error(
|
||||
`Provider [${name}] could not be RequestScoped or TransientScoped injectable if it contains event handlers.`
|
||||
);
|
||||
}
|
||||
|
||||
defs.forEach(({ event, opts }) => {
|
||||
this.on(
|
||||
event,
|
||||
(payload: any) => {
|
||||
// NOTE(@forehalo):
|
||||
// we might create spies on the event handlers when testing,
|
||||
// avoid reusing `fn` variable to fail the spies or stubs
|
||||
return instance[method](payload);
|
||||
},
|
||||
{
|
||||
...opts,
|
||||
name: signature,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
private readonly bindEventHandlers = once(() => {
|
||||
this.scanner.scan().forEach(({ event, handler, opts }) => {
|
||||
this.on(event, handler, opts);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { DiscoveryModule } from '@nestjs/core';
|
||||
import EventEmitter2 from 'eventemitter2';
|
||||
|
||||
import { EventBus, OnEvent } from './eventbus';
|
||||
import { EventBus } from './eventbus';
|
||||
import { EventHandlerScanner } from './scanner';
|
||||
|
||||
const EmitProvider = {
|
||||
provide: EventEmitter2,
|
||||
useValue: new EventEmitter2(),
|
||||
useFactory: () => new EventEmitter2(),
|
||||
};
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [DiscoveryModule],
|
||||
providers: [EventBus, EmitProvider],
|
||||
providers: [EventBus, EventHandlerScanner, EmitProvider],
|
||||
exports: [EventBus],
|
||||
})
|
||||
export class EventModule {}
|
||||
|
||||
export { EventBus, OnEvent };
|
||||
export { EventBus };
|
||||
export { OnEvent } from './def';
|
||||
|
||||
71
packages/backend/server/src/base/event/scanner.ts
Normal file
71
packages/backend/server/src/base/event/scanner.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { once } from 'lodash-es';
|
||||
|
||||
import { ModuleScanner } from '../nestjs';
|
||||
import {
|
||||
type EventName,
|
||||
type EventOptions,
|
||||
getEventHandlerMetadata,
|
||||
} from './def';
|
||||
|
||||
@Injectable()
|
||||
export class EventHandlerScanner {
|
||||
constructor(private readonly scanner: ModuleScanner) {}
|
||||
|
||||
scan = once(() => {
|
||||
const handlers: Array<{
|
||||
event: EventName;
|
||||
handler: (payload: any) => any;
|
||||
opts?: EventOptions;
|
||||
}> = [];
|
||||
const providers = this.scanner.getAtInjectables();
|
||||
|
||||
providers.forEach(wrapper => {
|
||||
const { instance, name } = wrapper;
|
||||
if (!instance || wrapper.isAlias) {
|
||||
return;
|
||||
}
|
||||
|
||||
const methods = this.scanner.getAllMethodNames(instance);
|
||||
|
||||
methods.forEach(method => {
|
||||
const fn = instance[method];
|
||||
|
||||
let defs = getEventHandlerMetadata(instance[method]);
|
||||
|
||||
if (defs.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const signature = `${name}.${method}`;
|
||||
|
||||
if (typeof fn !== 'function') {
|
||||
throw new Error(`Event handler [${signature}] is not a function.`);
|
||||
}
|
||||
|
||||
if (!wrapper.isDependencyTreeStatic()) {
|
||||
throw new Error(
|
||||
`Provider [${name}] could not be RequestScoped or TransientScoped injectable if it contains event handlers.`
|
||||
);
|
||||
}
|
||||
|
||||
defs.forEach(({ event, opts }) => {
|
||||
handlers.push({
|
||||
event,
|
||||
handler: (payload: any) => {
|
||||
// NOTE(@forehalo):
|
||||
// we might create spies on the event handlers when testing,
|
||||
// avoid reusing `fn` variable to fail the spies or stubs
|
||||
return instance[method].bind(instance)(payload);
|
||||
},
|
||||
opts: {
|
||||
name: signature,
|
||||
...opts,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
return handlers;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user