Skip to content

Commit

Permalink
refactor: server emit events (#11780)
Browse files Browse the repository at this point in the history
  • Loading branch information
jrasm91 committed Aug 15, 2024
1 parent 32c05ea commit 433c7ab
Show file tree
Hide file tree
Showing 27 changed files with 222 additions and 182 deletions.
22 changes: 17 additions & 5 deletions server/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE, ModuleRef } from '@ne
import { EventEmitterModule } from '@nestjs/event-emitter';
import { ScheduleModule, SchedulerRegistry } from '@nestjs/schedule';
import { TypeOrmModule } from '@nestjs/typeorm';
import _ from 'lodash';
import { ClsModule } from 'nestjs-cls';
import { OpenTelemetryModule } from 'nestjs-otel';
import { commands } from 'src/commands';
Expand All @@ -13,6 +14,7 @@ import { controllers } from 'src/controllers';
import { databaseConfig } from 'src/database.config';
import { entities } from 'src/entities';
import { IEventRepository } from 'src/interfaces/event.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { AuthGuard } from 'src/middleware/auth.guard';
import { ErrorInterceptor } from 'src/middleware/error.interceptor';
import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor';
Expand Down Expand Up @@ -54,15 +56,25 @@ export class ApiModule implements OnModuleInit, OnModuleDestroy {
constructor(
private moduleRef: ModuleRef,
@Inject(IEventRepository) private eventRepository: IEventRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository,
) {}

async onModuleInit() {
setupEventHandlers(this.moduleRef);
await this.eventRepository.emit('onBootstrapEvent', 'api');
const items = setupEventHandlers(this.moduleRef);

await this.eventRepository.emit('onBootstrap', 'api');

this.logger.setContext('EventLoader');
const eventMap = _.groupBy(items, 'event');
for (const [event, handlers] of Object.entries(eventMap)) {
for (const { priority, label } of handlers) {
this.logger.verbose(`Added ${event} {${label}${priority ? '' : ', ' + priority}} event`);
}
}
}

async onModuleDestroy() {
await this.eventRepository.emit('onShutdownEvent');
await this.eventRepository.emit('onShutdown');
}
}

Expand All @@ -78,11 +90,11 @@ export class MicroservicesModule implements OnModuleInit, OnModuleDestroy {

async onModuleInit() {
setupEventHandlers(this.moduleRef);
await this.eventRepository.emit('onBootstrapEvent', 'microservices');
await this.eventRepository.emit('onBootstrap', 'microservices');
}

async onModuleDestroy() {
await this.eventRepository.emit('onShutdownEvent');
await this.eventRepository.emit('onShutdown');
}
}

Expand Down
9 changes: 5 additions & 4 deletions server/src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { OnEventOptions } from '@nestjs/event-emitter/dist/interfaces';
import { ApiExtension, ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger';
import _ from 'lodash';
import { ADDED_IN_PREFIX, DEPRECATED_IN_PREFIX, LIFECYCLE_EXTENSION } from 'src/constants';
import { ServerEvent } from 'src/interfaces/event.interface';
import { EmitEvent, ServerEvent } from 'src/interfaces/event.interface';
import { Metadata } from 'src/middleware/auth.guard';
import { setUnion } from 'src/utils/set';

Expand Down Expand Up @@ -136,11 +136,12 @@ export const GenerateSql = (...options: GenerateSqlQueries[]) => SetMetadata(GEN
export const OnServerEvent = (event: ServerEvent, options?: OnEventOptions) =>
OnEvent(event, { suppressErrors: false, ...options });

export type HandlerOptions = {
export type EmitConfig = {
event: EmitEvent;
/** lower value has higher priority, defaults to 0 */
priority: number;
priority?: number;
};
export const EventHandlerOptions = (options: HandlerOptions) => SetMetadata(Metadata.EVENT_HANDLER_OPTIONS, options);
export const OnEmit = (config: EmitConfig) => SetMetadata(Metadata.ON_EMIT_CONFIG, config);

type LifecycleRelease = 'NEXT_RELEASE' | string;
type LifecycleMetadata = {
Expand Down
42 changes: 14 additions & 28 deletions server/src/interfaces/event.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,27 @@ import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.d

export const IEventRepository = 'IEventRepository';

export type SystemConfigUpdateEvent = { newConfig: SystemConfig; oldConfig: SystemConfig };
export type AlbumUpdateEvent = {
id: string;
/** user id */
updatedBy: string;
};
export type AlbumInviteEvent = { id: string; userId: string };
export type UserSignupEvent = { notify: boolean; id: string; tempPassword?: string };

type MaybePromise<T> = Promise<T> | T;
type Handler<T = undefined> = (data: T) => MaybePromise<void>;

const noop = () => {};
const dummyHandlers = {
type EmitEventMap = {
// app events
onBootstrapEvent: noop as Handler<'api' | 'microservices'>,
onShutdownEvent: noop as () => MaybePromise<void>,
onBootstrap: ['api' | 'microservices'];
onShutdown: [];

// config events
onConfigUpdateEvent: noop as Handler<SystemConfigUpdateEvent>,
onConfigValidateEvent: noop as Handler<SystemConfigUpdateEvent>,
onConfigUpdate: [{ newConfig: SystemConfig; oldConfig: SystemConfig }];
onConfigValidate: [{ newConfig: SystemConfig; oldConfig: SystemConfig }];

// album events
onAlbumUpdateEvent: noop as Handler<AlbumUpdateEvent>,
onAlbumInviteEvent: noop as Handler<AlbumInviteEvent>,
onAlbumUpdate: [{ id: string; updatedBy: string }];
onAlbumInvite: [{ id: string; userId: string }];

// user events
onUserSignupEvent: noop as Handler<UserSignupEvent>,
onUserSignup: [{ notify: boolean; id: string; tempPassword?: string }];
};

export type EventHandlers = typeof dummyHandlers;
export type EmitEvent = keyof EventHandlers;
export type EmitEventHandler<T extends EmitEvent> = (...args: Parameters<EventHandlers[T]>) => MaybePromise<void>;
export const events = Object.keys(dummyHandlers) as EmitEvent[];
export type OnEvents = Partial<EventHandlers>;
export type EmitEvent = keyof EmitEventMap;
export type EmitHandler<T extends EmitEvent> = (...args: ArgsOf<T>) => Promise<void> | void;
export type ArgOf<T extends EmitEvent> = EmitEventMap[T][0];
export type ArgsOf<T extends EmitEvent> = EmitEventMap[T];

export enum ClientEvent {
UPLOAD_SUCCESS = 'on_upload_success',
Expand Down Expand Up @@ -81,8 +67,8 @@ export interface ServerEventMap {
}

export interface IEventRepository {
on<T extends EmitEvent>(event: T, handler: EmitEventHandler<T>): void;
emit<T extends EmitEvent>(event: T, ...args: Parameters<EmitEventHandler<T>>): Promise<void>;
on<T extends keyof EmitEventMap>(event: T, handler: EmitHandler<T>): void;
emit<T extends keyof EmitEventMap>(event: T, ...args: ArgsOf<T>): Promise<void>;

/**
* Send to connected clients for a specific user
Expand Down
2 changes: 1 addition & 1 deletion server/src/middleware/auth.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export enum Metadata {
ADMIN_ROUTE = 'admin_route',
SHARED_ROUTE = 'shared_route',
API_KEY_SECURITY = 'api_key',
EVENT_HANDLER_OPTIONS = 'event_handler_options',
ON_EMIT_CONFIG = 'on_emit_config',
}

type AdminRoute = { admin?: true };
Expand Down
18 changes: 12 additions & 6 deletions server/src/repositories/event.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import {
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import {
ArgsOf,
ClientEventMap,
EmitEvent,
EmitEventHandler,
EmitHandler,
IEventRepository,
ServerEvent,
ServerEventMap,
Expand All @@ -20,6 +21,8 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { AuthService } from 'src/services/auth.service';
import { Instrumentation } from 'src/utils/instrumentation';

type EmitHandlers = Partial<{ [T in EmitEvent]: EmitHandler<T>[] }>;

@Instrumentation()
@WebSocketGateway({
cors: true,
Expand All @@ -28,7 +31,7 @@ import { Instrumentation } from 'src/utils/instrumentation';
})
@Injectable()
export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit, IEventRepository {
private emitHandlers: Partial<Record<EmitEvent, EmitEventHandler<EmitEvent>[]>> = {};
private emitHandlers: EmitHandlers = {};

@WebSocketServer()
private server?: Server;
Expand Down Expand Up @@ -78,12 +81,15 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect
await client.leave(client.nsp.name);
}

on<T extends EmitEvent>(event: T, handler: EmitEventHandler<T>): void {
const handlers: EmitEventHandler<EmitEvent>[] = this.emitHandlers[event] || [];
this.emitHandlers[event] = [...handlers, handler];
on<T extends EmitEvent>(event: T, handler: EmitHandler<T>): void {
if (!this.emitHandlers[event]) {
this.emitHandlers[event] = [];
}

this.emitHandlers[event].push(handler);
}

async emit<T extends EmitEvent>(event: T, ...args: Parameters<EmitEventHandler<T>>): Promise<void> {
async emit<T extends EmitEvent>(event: T, ...args: ArgsOf<T>): Promise<void> {
const handlers = this.emitHandlers[event] || [];
for (const handler of handlers) {
await handler(...args);
Expand Down
6 changes: 3 additions & 3 deletions server/src/services/album.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ describe(AlbumService.name, () => {
userId: authStub.user2.user.id,
albumId: albumStub.sharedWithAdmin.id,
});
expect(eventMock.emit).toHaveBeenCalledWith('onAlbumInviteEvent', {
expect(eventMock.emit).toHaveBeenCalledWith('onAlbumInvite', {
id: albumStub.sharedWithAdmin.id,
userId: userStub.user2.id,
});
Expand Down Expand Up @@ -568,7 +568,7 @@ describe(AlbumService.name, () => {
albumThumbnailAssetId: 'asset-1',
});
expect(albumMock.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']);
expect(eventMock.emit).toHaveBeenCalledWith('onAlbumUpdateEvent', {
expect(eventMock.emit).toHaveBeenCalledWith('onAlbumUpdate', {
id: 'album-123',
updatedBy: authStub.admin.user.id,
});
Expand Down Expand Up @@ -612,7 +612,7 @@ describe(AlbumService.name, () => {
albumThumbnailAssetId: 'asset-1',
});
expect(albumMock.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']);
expect(eventMock.emit).toHaveBeenCalledWith('onAlbumUpdateEvent', {
expect(eventMock.emit).toHaveBeenCalledWith('onAlbumUpdate', {
id: 'album-123',
updatedBy: authStub.user1.user.id,
});
Expand Down
4 changes: 2 additions & 2 deletions server/src/services/album.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ export class AlbumService {
albumThumbnailAssetId: album.albumThumbnailAssetId ?? firstNewAssetId,
});

await this.eventRepository.emit('onAlbumUpdateEvent', { id, updatedBy: auth.user.id });
await this.eventRepository.emit('onAlbumUpdate', { id, updatedBy: auth.user.id });
}

return results;
Expand Down Expand Up @@ -235,7 +235,7 @@ export class AlbumService {
}

await this.albumUserRepository.create({ userId: userId, albumId: id, role });
await this.eventRepository.emit('onAlbumInviteEvent', { id, userId });
await this.eventRepository.emit('onAlbumInvite', { id, userId });
}

return this.findOrFail(id, { withAssets: true }).then(mapAlbumWithoutAssets);
Expand Down
Loading

0 comments on commit 433c7ab

Please sign in to comment.