refactor(server): client events (#13062)

This commit is contained in:
Jason Rasmussen 2024-09-30 15:50:34 -04:00 committed by GitHub
parent 47821cda35
commit dfc2d5002b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 48 additions and 64 deletions

View File

@ -62,36 +62,20 @@ export type EmitHandler<T extends EmitEvent> = (...args: ArgsOf<T>) => Promise<v
export type ArgOf<T extends EmitEvent> = EventMap[T][0]; export type ArgOf<T extends EmitEvent> = EventMap[T][0];
export type ArgsOf<T extends EmitEvent> = EventMap[T]; export type ArgsOf<T extends EmitEvent> = 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 { export interface ClientEventMap {
[ClientEvent.UPLOAD_SUCCESS]: AssetResponseDto; on_upload_success: [AssetResponseDto];
[ClientEvent.USER_DELETE]: string; on_user_delete: [string];
[ClientEvent.ASSET_DELETE]: string; on_asset_delete: [string];
[ClientEvent.ASSET_TRASH]: string[]; on_asset_trash: [string[]];
[ClientEvent.ASSET_UPDATE]: AssetResponseDto; on_asset_update: [AssetResponseDto];
[ClientEvent.ASSET_HIDDEN]: string; on_asset_hidden: [string];
[ClientEvent.ASSET_RESTORE]: string[]; on_asset_restore: [string[]];
[ClientEvent.ASSET_STACK_UPDATE]: string[]; on_asset_stack_update: string[];
[ClientEvent.PERSON_THUMBNAIL]: string; on_person_thumbnail: [string];
[ClientEvent.SERVER_VERSION]: ServerVersionResponseDto; on_server_version: [ServerVersionResponseDto];
[ClientEvent.CONFIG_UPDATE]: Record<string, never>; on_config_update: [];
[ClientEvent.NEW_RELEASE]: ReleaseNotification; on_new_release: [ReleaseNotification];
[ClientEvent.SESSION_DELETE]: string; on_session_delete: [string];
} }
export type EventItem<T extends EmitEvent> = { export type EventItem<T extends EmitEvent> = {
@ -107,11 +91,11 @@ export interface IEventRepository {
/** /**
* Send to connected clients for a specific user * Send to connected clients for a specific user
*/ */
clientSend<E extends keyof ClientEventMap>(event: E, room: string, data: ClientEventMap[E]): void; clientSend<E extends keyof ClientEventMap>(event: E, room: string, ...data: ClientEventMap[E]): void;
/** /**
* Send to all connected clients * Send to all connected clients
*/ */
clientBroadcast<E extends keyof ClientEventMap>(event: E, data: ClientEventMap[E]): void; clientBroadcast<E extends keyof ClientEventMap>(event: E, ...data: ClientEventMap[E]): void;
/** /**
* Send to all connected servers * Send to all connected servers
*/ */

View File

@ -106,12 +106,12 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect
} }
} }
clientSend<E extends keyof ClientEventMap>(event: E, room: string, data: ClientEventMap[E]) { clientSend<T extends keyof ClientEventMap>(event: T, room: string, ...data: ClientEventMap[T]) {
this.server?.to(room).emit(event, data); this.server?.to(room).emit(event, ...data);
} }
clientBroadcast<E extends keyof ClientEventMap>(event: E, data: ClientEventMap[E]) { clientBroadcast<T extends keyof ClientEventMap>(event: T, ...data: ClientEventMap[T]) {
this.server?.emit(event, data); this.server?.emit(event, ...data);
} }
serverSend<T extends ServerEvents>(event: T, ...args: ArgsOf<T>): void { serverSend<T extends ServerEvents>(event: T, ...args: ArgsOf<T>): void {

View File

@ -6,7 +6,7 @@ import { mapAsset } from 'src/dtos/asset-response.dto';
import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto'; import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto';
import { AssetType, ManualJobName } from 'src/enum'; import { AssetType, ManualJobName } from 'src/enum';
import { IAssetRepository } from 'src/interfaces/asset.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 { import {
ConcurrentQueueName, ConcurrentQueueName,
IJobRepository, IJobRepository,
@ -279,7 +279,7 @@ export class JobService {
if (item.data.source === 'sidecar-write') { if (item.data.source === 'sidecar-write') {
const [asset] = await this.assetRepository.getByIdsWithAllRelations([item.data.id]); const [asset] = await this.assetRepository.getByIdsWithAllRelations([item.data.id]);
if (asset) { 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 }); await this.jobRepository.queue({ name: JobName.LINK_LIVE_PHOTOS, data: item.data });
@ -302,7 +302,7 @@ export class JobService {
const { id } = item.data; const { id } = item.data;
const person = await this.personRepository.getById(id); const person = await this.personRepository.getById(id);
if (person) { if (person) {
this.eventRepository.clientSend(ClientEvent.PERSON_THUMBNAIL, person.ownerId, person.id); this.eventRepository.clientSend('on_person_thumbnail', person.ownerId, person.id);
} }
break; break;
} }
@ -331,7 +331,7 @@ export class JobService {
await this.jobRepository.queueAll(jobs); await this.jobRepository.queueAll(jobs);
if (asset.isVisible) { if (asset.isVisible) {
this.eventRepository.clientSend(ClientEvent.UPLOAD_SUCCESS, asset.ownerId, mapAsset(asset)); this.eventRepository.clientSend('on_upload_success', asset.ownerId, mapAsset(asset));
} }
break; break;
@ -345,7 +345,7 @@ export class JobService {
} }
case JobName.USER_DELETION: { case JobName.USER_DELETION: {
this.eventRepository.clientBroadcast(ClientEvent.USER_DELETE, item.data.id); this.eventRepository.clientBroadcast('on_user_delete', item.data.id);
break; break;
} }
} }

View File

@ -6,7 +6,7 @@ import { AssetFileEntity } from 'src/entities/asset-files.entity';
import { AssetFileType, UserMetadataKey } from 'src/enum'; import { AssetFileType, UserMetadataKey } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IAssetRepository } from 'src/interfaces/asset.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 { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { EmailTemplate, INotificationRepository } from 'src/interfaces/notification.interface'; import { EmailTemplate, INotificationRepository } from 'src/interfaces/notification.interface';
@ -104,7 +104,7 @@ describe(NotificationService.name, () => {
it('should emit client and server events', () => { it('should emit client and server events', () => {
const update = { newConfig: defaults }; const update = { newConfig: defaults };
expect(sut.onConfigUpdate(update)).toBeUndefined(); 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); expect(eventMock.serverSend).toHaveBeenCalledWith('config.update', update);
}); });
}); });
@ -236,28 +236,28 @@ describe(NotificationService.name, () => {
describe('onStackCreate', () => { describe('onStackCreate', () => {
it('should send connected clients an event', () => { it('should send connected clients an event', () => {
sut.onStackCreate({ stackId: 'stack-id', userId: 'user-id' }); 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', () => { describe('onStackUpdate', () => {
it('should send connected clients an event', () => { it('should send connected clients an event', () => {
sut.onStackUpdate({ stackId: 'stack-id', userId: 'user-id' }); 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', () => { describe('onStackDelete', () => {
it('should send connected clients an event', () => { it('should send connected clients an event', () => {
sut.onStackDelete({ stackId: 'stack-id', userId: 'user-id' }); 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', () => { describe('onStacksDelete', () => {
it('should send connected clients an event', () => { it('should send connected clients an event', () => {
sut.onStacksDelete({ stackIds: ['stack-id'], userId: 'user-id' }); 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');
}); });
}); });

View File

@ -6,7 +6,7 @@ import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
import { AlbumEntity } from 'src/entities/album.entity'; import { AlbumEntity } from 'src/entities/album.entity';
import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IAssetRepository } from 'src/interfaces/asset.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 { import {
IEmailJob, IEmailJob,
IJobRepository, IJobRepository,
@ -45,7 +45,7 @@ export class NotificationService {
@OnEvent({ name: 'config.update' }) @OnEvent({ name: 'config.update' })
onConfigUpdate({ oldConfig, newConfig }: ArgOf<'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 }); this.eventRepository.serverSend('config.update', { oldConfig, newConfig });
} }
@ -66,7 +66,7 @@ export class NotificationService {
@OnEvent({ name: 'asset.hide' }) @OnEvent({ name: 'asset.hide' })
onAssetHide({ assetId, userId }: ArgOf<'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' }) @OnEvent({ name: 'asset.show' })
@ -76,42 +76,42 @@ export class NotificationService {
@OnEvent({ name: 'asset.trash' }) @OnEvent({ name: 'asset.trash' })
onAssetTrash({ assetId, userId }: ArgOf<'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' }) @OnEvent({ name: 'asset.delete' })
onAssetDelete({ assetId, userId }: ArgOf<'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' }) @OnEvent({ name: 'assets.trash' })
onAssetsTrash({ assetIds, userId }: ArgOf<'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' }) @OnEvent({ name: 'assets.restore' })
onAssetsRestore({ assetIds, userId }: ArgOf<'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' }) @OnEvent({ name: 'stack.create' })
onStackCreate({ userId }: ArgOf<'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' }) @OnEvent({ name: 'stack.update' })
onStackUpdate({ userId }: ArgOf<'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' }) @OnEvent({ name: 'stack.delete' })
onStackDelete({ userId }: ArgOf<'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' }) @OnEvent({ name: 'stacks.delete' })
onStacksDelete({ userId }: ArgOf<'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' }) @OnEvent({ name: 'user.signup' })
@ -134,7 +134,7 @@ export class NotificationService {
@OnEvent({ name: 'session.delete' }) @OnEvent({ name: 'session.delete' })
onSessionDelete({ sessionId }: ArgOf<'session.delete'>) { onSessionDelete({ sessionId }: ArgOf<'session.delete'>) {
// after the response is sent // 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) { async sendTestEmail(id: string, dto: SystemConfigSmtpDto) {

View File

@ -7,7 +7,7 @@ import { OnEvent } from 'src/decorators';
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto'; import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
import { VersionCheckMetadata } from 'src/entities/system-metadata.entity'; import { VersionCheckMetadata } from 'src/entities/system-metadata.entity';
import { SystemMetadataKey } from 'src/enum'; 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 { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
@ -80,7 +80,7 @@ export class VersionService {
if (semver.gt(releaseVersion, serverVersion)) { if (semver.gt(releaseVersion, serverVersion)) {
this.logger.log(`Found ${releaseVersion}, released at ${new Date(publishedAt).toLocaleString()}`); 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) { } catch (error: Error | any) {
this.logger.warn(`Unable to run version check: ${error}`, error?.stack); this.logger.warn(`Unable to run version check: ${error}`, error?.stack);
@ -92,10 +92,10 @@ export class VersionService {
@OnEvent({ name: 'websocket.connect' }) @OnEvent({ name: 'websocket.connect' })
async onWebsocketConnection({ userId }: ArgOf<'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); const metadata = await this.systemMetadataRepository.get(SystemMetadataKey.VERSION_CHECK_STATE);
if (metadata) { if (metadata) {
this.eventRepository.clientSend(ClientEvent.NEW_RELEASE, userId, asNotification(metadata)); this.eventRepository.clientSend('on_new_release', userId, asNotification(metadata));
} }
} }
} }

View File

@ -5,8 +5,8 @@ export const newEventRepositoryMock = (): Mocked<IEventRepository> => {
return { return {
on: vitest.fn() as any, on: vitest.fn() as any,
emit: vitest.fn() as any, emit: vitest.fn() as any,
clientSend: vitest.fn(), clientSend: vitest.fn() as any,
clientBroadcast: vitest.fn(), clientBroadcast: vitest.fn() as any,
serverSend: vitest.fn(), serverSend: vitest.fn(),
}; };
}; };