From dfc2d5002b6c560150fd77e1d48b6b54d8315266 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Mon, 30 Sep 2024 15:50:34 -0400 Subject: [PATCH] refactor(server): client events (#13062) --- server/src/interfaces/event.interface.ts | 46 ++++++------------- server/src/repositories/event.repository.ts | 8 ++-- server/src/services/job.service.ts | 10 ++-- .../src/services/notification.service.spec.ts | 12 ++--- server/src/services/notification.service.ts | 24 +++++----- server/src/services/version.service.ts | 8 ++-- .../repositories/event.repository.mock.ts | 4 +- 7 files changed, 48 insertions(+), 64 deletions(-) diff --git a/server/src/interfaces/event.interface.ts b/server/src/interfaces/event.interface.ts index 02027d87e6..a125e47ada 100644 --- a/server/src/interfaces/event.interface.ts +++ b/server/src/interfaces/event.interface.ts @@ -62,36 +62,20 @@ export type EmitHandler = (...args: ArgsOf) => Promise = EventMap[T][0]; export type ArgsOf = EventMap[T]; -export enum ClientEvent { - UPLOAD_SUCCESS = 'on_upload_success', - USER_DELETE = 'on_user_delete', - ASSET_DELETE = 'on_asset_delete', - ASSET_TRASH = 'on_asset_trash', - ASSET_UPDATE = 'on_asset_update', - ASSET_HIDDEN = 'on_asset_hidden', - ASSET_RESTORE = 'on_asset_restore', - ASSET_STACK_UPDATE = 'on_asset_stack_update', - PERSON_THUMBNAIL = 'on_person_thumbnail', - SERVER_VERSION = 'on_server_version', - CONFIG_UPDATE = 'on_config_update', - NEW_RELEASE = 'on_new_release', - SESSION_DELETE = 'on_session_delete', -} - export interface ClientEventMap { - [ClientEvent.UPLOAD_SUCCESS]: AssetResponseDto; - [ClientEvent.USER_DELETE]: string; - [ClientEvent.ASSET_DELETE]: string; - [ClientEvent.ASSET_TRASH]: string[]; - [ClientEvent.ASSET_UPDATE]: AssetResponseDto; - [ClientEvent.ASSET_HIDDEN]: string; - [ClientEvent.ASSET_RESTORE]: string[]; - [ClientEvent.ASSET_STACK_UPDATE]: string[]; - [ClientEvent.PERSON_THUMBNAIL]: string; - [ClientEvent.SERVER_VERSION]: ServerVersionResponseDto; - [ClientEvent.CONFIG_UPDATE]: Record; - [ClientEvent.NEW_RELEASE]: ReleaseNotification; - [ClientEvent.SESSION_DELETE]: string; + on_upload_success: [AssetResponseDto]; + on_user_delete: [string]; + on_asset_delete: [string]; + on_asset_trash: [string[]]; + on_asset_update: [AssetResponseDto]; + on_asset_hidden: [string]; + on_asset_restore: [string[]]; + on_asset_stack_update: string[]; + on_person_thumbnail: [string]; + on_server_version: [ServerVersionResponseDto]; + on_config_update: []; + on_new_release: [ReleaseNotification]; + on_session_delete: [string]; } export type EventItem = { @@ -107,11 +91,11 @@ export interface IEventRepository { /** * Send to connected clients for a specific user */ - clientSend(event: E, room: string, data: ClientEventMap[E]): void; + clientSend(event: E, room: string, ...data: ClientEventMap[E]): void; /** * Send to all connected clients */ - clientBroadcast(event: E, data: ClientEventMap[E]): void; + clientBroadcast(event: E, ...data: ClientEventMap[E]): void; /** * Send to all connected servers */ diff --git a/server/src/repositories/event.repository.ts b/server/src/repositories/event.repository.ts index a8b2fa67c3..90d8e7bf5d 100644 --- a/server/src/repositories/event.repository.ts +++ b/server/src/repositories/event.repository.ts @@ -106,12 +106,12 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect } } - clientSend(event: E, room: string, data: ClientEventMap[E]) { - this.server?.to(room).emit(event, data); + clientSend(event: T, room: string, ...data: ClientEventMap[T]) { + this.server?.to(room).emit(event, ...data); } - clientBroadcast(event: E, data: ClientEventMap[E]) { - this.server?.emit(event, data); + clientBroadcast(event: T, ...data: ClientEventMap[T]) { + this.server?.emit(event, ...data); } serverSend(event: T, ...args: ArgsOf): void { diff --git a/server/src/services/job.service.ts b/server/src/services/job.service.ts index 68da13a8e4..159efdf023 100644 --- a/server/src/services/job.service.ts +++ b/server/src/services/job.service.ts @@ -6,7 +6,7 @@ import { mapAsset } from 'src/dtos/asset-response.dto'; import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto'; import { AssetType, ManualJobName } from 'src/enum'; import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { ArgOf, ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; +import { ArgOf, IEventRepository } from 'src/interfaces/event.interface'; import { ConcurrentQueueName, IJobRepository, @@ -279,7 +279,7 @@ export class JobService { if (item.data.source === 'sidecar-write') { const [asset] = await this.assetRepository.getByIdsWithAllRelations([item.data.id]); if (asset) { - this.eventRepository.clientSend(ClientEvent.ASSET_UPDATE, asset.ownerId, mapAsset(asset)); + this.eventRepository.clientSend('on_asset_update', asset.ownerId, mapAsset(asset)); } } await this.jobRepository.queue({ name: JobName.LINK_LIVE_PHOTOS, data: item.data }); @@ -302,7 +302,7 @@ export class JobService { const { id } = item.data; const person = await this.personRepository.getById(id); if (person) { - this.eventRepository.clientSend(ClientEvent.PERSON_THUMBNAIL, person.ownerId, person.id); + this.eventRepository.clientSend('on_person_thumbnail', person.ownerId, person.id); } break; } @@ -331,7 +331,7 @@ export class JobService { await this.jobRepository.queueAll(jobs); if (asset.isVisible) { - this.eventRepository.clientSend(ClientEvent.UPLOAD_SUCCESS, asset.ownerId, mapAsset(asset)); + this.eventRepository.clientSend('on_upload_success', asset.ownerId, mapAsset(asset)); } break; @@ -345,7 +345,7 @@ export class JobService { } case JobName.USER_DELETION: { - this.eventRepository.clientBroadcast(ClientEvent.USER_DELETE, item.data.id); + this.eventRepository.clientBroadcast('on_user_delete', item.data.id); break; } } diff --git a/server/src/services/notification.service.spec.ts b/server/src/services/notification.service.spec.ts index 106f0be082..5fba38d1eb 100644 --- a/server/src/services/notification.service.spec.ts +++ b/server/src/services/notification.service.spec.ts @@ -6,7 +6,7 @@ import { AssetFileEntity } from 'src/entities/asset-files.entity'; import { AssetFileType, UserMetadataKey } from 'src/enum'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; +import { IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { EmailTemplate, INotificationRepository } from 'src/interfaces/notification.interface'; @@ -104,7 +104,7 @@ describe(NotificationService.name, () => { it('should emit client and server events', () => { const update = { newConfig: defaults }; expect(sut.onConfigUpdate(update)).toBeUndefined(); - expect(eventMock.clientBroadcast).toHaveBeenCalledWith(ClientEvent.CONFIG_UPDATE, {}); + expect(eventMock.clientBroadcast).toHaveBeenCalledWith('on_config_update'); expect(eventMock.serverSend).toHaveBeenCalledWith('config.update', update); }); }); @@ -236,28 +236,28 @@ describe(NotificationService.name, () => { describe('onStackCreate', () => { it('should send connected clients an event', () => { sut.onStackCreate({ stackId: 'stack-id', userId: 'user-id' }); - expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id', []); + expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id'); }); }); describe('onStackUpdate', () => { it('should send connected clients an event', () => { sut.onStackUpdate({ stackId: 'stack-id', userId: 'user-id' }); - expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id', []); + expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id'); }); }); describe('onStackDelete', () => { it('should send connected clients an event', () => { sut.onStackDelete({ stackId: 'stack-id', userId: 'user-id' }); - expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id', []); + expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id'); }); }); describe('onStacksDelete', () => { it('should send connected clients an event', () => { sut.onStacksDelete({ stackIds: ['stack-id'], userId: 'user-id' }); - expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id', []); + expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id'); }); }); diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts index 626e536c40..a3adfa4565 100644 --- a/server/src/services/notification.service.ts +++ b/server/src/services/notification.service.ts @@ -6,7 +6,7 @@ import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto'; import { AlbumEntity } from 'src/entities/album.entity'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { ArgOf, ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; +import { ArgOf, IEventRepository } from 'src/interfaces/event.interface'; import { IEmailJob, IJobRepository, @@ -45,7 +45,7 @@ export class NotificationService { @OnEvent({ name: 'config.update' }) onConfigUpdate({ oldConfig, newConfig }: ArgOf<'config.update'>) { - this.eventRepository.clientBroadcast(ClientEvent.CONFIG_UPDATE, {}); + this.eventRepository.clientBroadcast('on_config_update'); this.eventRepository.serverSend('config.update', { oldConfig, newConfig }); } @@ -66,7 +66,7 @@ export class NotificationService { @OnEvent({ name: 'asset.hide' }) onAssetHide({ assetId, userId }: ArgOf<'asset.hide'>) { - this.eventRepository.clientSend(ClientEvent.ASSET_HIDDEN, userId, assetId); + this.eventRepository.clientSend('on_asset_hidden', userId, assetId); } @OnEvent({ name: 'asset.show' }) @@ -76,42 +76,42 @@ export class NotificationService { @OnEvent({ name: 'asset.trash' }) onAssetTrash({ assetId, userId }: ArgOf<'asset.trash'>) { - this.eventRepository.clientSend(ClientEvent.ASSET_TRASH, userId, [assetId]); + this.eventRepository.clientSend('on_asset_trash', userId, [assetId]); } @OnEvent({ name: 'asset.delete' }) onAssetDelete({ assetId, userId }: ArgOf<'asset.delete'>) { - this.eventRepository.clientSend(ClientEvent.ASSET_DELETE, userId, assetId); + this.eventRepository.clientSend('on_asset_delete', userId, assetId); } @OnEvent({ name: 'assets.trash' }) onAssetsTrash({ assetIds, userId }: ArgOf<'assets.trash'>) { - this.eventRepository.clientSend(ClientEvent.ASSET_TRASH, userId, assetIds); + this.eventRepository.clientSend('on_asset_trash', userId, assetIds); } @OnEvent({ name: 'assets.restore' }) onAssetsRestore({ assetIds, userId }: ArgOf<'assets.restore'>) { - this.eventRepository.clientSend(ClientEvent.ASSET_RESTORE, userId, assetIds); + this.eventRepository.clientSend('on_asset_restore', userId, assetIds); } @OnEvent({ name: 'stack.create' }) onStackCreate({ userId }: ArgOf<'stack.create'>) { - this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, userId, []); + this.eventRepository.clientSend('on_asset_stack_update', userId); } @OnEvent({ name: 'stack.update' }) onStackUpdate({ userId }: ArgOf<'stack.update'>) { - this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, userId, []); + this.eventRepository.clientSend('on_asset_stack_update', userId); } @OnEvent({ name: 'stack.delete' }) onStackDelete({ userId }: ArgOf<'stack.delete'>) { - this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, userId, []); + this.eventRepository.clientSend('on_asset_stack_update', userId); } @OnEvent({ name: 'stacks.delete' }) onStacksDelete({ userId }: ArgOf<'stacks.delete'>) { - this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, userId, []); + this.eventRepository.clientSend('on_asset_stack_update', userId); } @OnEvent({ name: 'user.signup' }) @@ -134,7 +134,7 @@ export class NotificationService { @OnEvent({ name: 'session.delete' }) onSessionDelete({ sessionId }: ArgOf<'session.delete'>) { // after the response is sent - setTimeout(() => this.eventRepository.clientSend(ClientEvent.SESSION_DELETE, sessionId, sessionId), 500); + setTimeout(() => this.eventRepository.clientSend('on_session_delete', sessionId, sessionId), 500); } async sendTestEmail(id: string, dto: SystemConfigSmtpDto) { diff --git a/server/src/services/version.service.ts b/server/src/services/version.service.ts index 0c7ae52cac..1d2785356e 100644 --- a/server/src/services/version.service.ts +++ b/server/src/services/version.service.ts @@ -7,7 +7,7 @@ import { OnEvent } from 'src/decorators'; import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto'; import { VersionCheckMetadata } from 'src/entities/system-metadata.entity'; import { SystemMetadataKey } from 'src/enum'; -import { ArgOf, ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; +import { ArgOf, IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; @@ -80,7 +80,7 @@ export class VersionService { if (semver.gt(releaseVersion, serverVersion)) { this.logger.log(`Found ${releaseVersion}, released at ${new Date(publishedAt).toLocaleString()}`); - this.eventRepository.clientBroadcast(ClientEvent.NEW_RELEASE, asNotification(metadata)); + this.eventRepository.clientBroadcast('on_new_release', asNotification(metadata)); } } catch (error: Error | any) { this.logger.warn(`Unable to run version check: ${error}`, error?.stack); @@ -92,10 +92,10 @@ export class VersionService { @OnEvent({ name: 'websocket.connect' }) async onWebsocketConnection({ userId }: ArgOf<'websocket.connect'>) { - this.eventRepository.clientSend(ClientEvent.SERVER_VERSION, userId, serverVersion); + this.eventRepository.clientSend('on_server_version', userId, serverVersion); const metadata = await this.systemMetadataRepository.get(SystemMetadataKey.VERSION_CHECK_STATE); if (metadata) { - this.eventRepository.clientSend(ClientEvent.NEW_RELEASE, userId, asNotification(metadata)); + this.eventRepository.clientSend('on_new_release', userId, asNotification(metadata)); } } } diff --git a/server/test/repositories/event.repository.mock.ts b/server/test/repositories/event.repository.mock.ts index 78c62e95f2..6893b29f49 100644 --- a/server/test/repositories/event.repository.mock.ts +++ b/server/test/repositories/event.repository.mock.ts @@ -5,8 +5,8 @@ export const newEventRepositoryMock = (): Mocked => { return { on: vitest.fn() as any, emit: vitest.fn() as any, - clientSend: vitest.fn(), - clientBroadcast: vitest.fn(), + clientSend: vitest.fn() as any, + clientBroadcast: vitest.fn() as any, serverSend: vitest.fn(), }; };