Skip to content

Commit 5d0a4bb

Browse files
authored
refactor(server): app module (immich-app#13193)
1 parent 7ee0221 commit 5d0a4bb

18 files changed

+122
-130
lines changed

server/src/app.module.ts

+27-39
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import { ConfigModule } from '@nestjs/config';
44
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE, ModuleRef } from '@nestjs/core';
55
import { ScheduleModule, SchedulerRegistry } from '@nestjs/schedule';
66
import { TypeOrmModule } from '@nestjs/typeorm';
7-
import _ from 'lodash';
87
import { ClsModule } from 'nestjs-cls';
98
import { OpenTelemetryModule } from 'nestjs-otel';
109
import { commands } from 'src/commands';
1110
import { bullConfig, bullQueues, clsConfig, immichAppConfig } from 'src/config';
1211
import { controllers } from 'src/controllers';
1312
import { databaseConfig } from 'src/database.config';
1413
import { entities } from 'src/entities';
14+
import { ImmichWorker } from 'src/enum';
1515
import { IEventRepository } from 'src/interfaces/event.interface';
1616
import { ILoggerRepository } from 'src/interfaces/logger.interface';
1717
import { AuthGuard } from 'src/middleware/auth.guard';
@@ -22,7 +22,6 @@ import { LoggingInterceptor } from 'src/middleware/logging.interceptor';
2222
import { repositories } from 'src/repositories';
2323
import { services } from 'src/services';
2424
import { DatabaseService } from 'src/services/database.service';
25-
import { setupEventHandlers } from 'src/utils/events';
2625
import { otelConfig } from 'src/utils/instrumentation';
2726

2827
const common = [...services, ...repositories];
@@ -56,59 +55,48 @@ const imports = [
5655
TypeOrmModule.forFeature(entities),
5756
];
5857

59-
@Module({
60-
imports: [...imports, ScheduleModule.forRoot()],
61-
controllers: [...controllers],
62-
providers: [...common, ...middleware],
63-
})
64-
export class ApiModule implements OnModuleInit, OnModuleDestroy {
58+
abstract class BaseModule implements OnModuleInit, OnModuleDestroy {
59+
private get worker() {
60+
return this.getWorker();
61+
}
62+
6563
constructor(
66-
private moduleRef: ModuleRef,
64+
@Inject(ILoggerRepository) logger: ILoggerRepository,
6765
@Inject(IEventRepository) private eventRepository: IEventRepository,
68-
@Inject(ILoggerRepository) private logger: ILoggerRepository,
6966
) {
70-
logger.setAppName('Api');
67+
logger.setAppName(this.worker);
7168
}
7269

73-
async onModuleInit() {
74-
const items = setupEventHandlers(this.moduleRef);
75-
76-
await this.eventRepository.emit('app.bootstrap', 'api');
70+
abstract getWorker(): ImmichWorker;
7771

78-
this.logger.setContext('EventLoader');
79-
const eventMap = _.groupBy(items, 'event');
80-
for (const [event, handlers] of Object.entries(eventMap)) {
81-
for (const { priority, label } of handlers) {
82-
this.logger.verbose(`Added ${event} {${label}${priority ? '' : ', ' + priority}} event`);
83-
}
84-
}
72+
async onModuleInit() {
73+
this.eventRepository.setup({ services });
74+
await this.eventRepository.emit('app.bootstrap', this.worker);
8575
}
8676

8777
async onModuleDestroy() {
88-
await this.eventRepository.emit('app.shutdown');
78+
await this.eventRepository.emit('app.shutdown', this.worker);
8979
}
9080
}
9181

9282
@Module({
93-
imports: [...imports],
94-
providers: [...common, SchedulerRegistry],
83+
imports: [...imports, ScheduleModule.forRoot()],
84+
controllers: [...controllers],
85+
providers: [...common, ...middleware],
9586
})
96-
export class MicroservicesModule implements OnModuleInit, OnModuleDestroy {
97-
constructor(
98-
private moduleRef: ModuleRef,
99-
@Inject(IEventRepository) private eventRepository: IEventRepository,
100-
@Inject(ILoggerRepository) logger: ILoggerRepository,
101-
) {
102-
logger.setAppName('Microservices');
103-
}
104-
105-
async onModuleInit() {
106-
setupEventHandlers(this.moduleRef);
107-
await this.eventRepository.emit('app.bootstrap', 'microservices');
87+
export class ApiModule extends BaseModule {
88+
getWorker() {
89+
return ImmichWorker.API;
10890
}
91+
}
10992

110-
async onModuleDestroy() {
111-
await this.eventRepository.emit('app.shutdown');
93+
@Module({
94+
imports: [...imports],
95+
providers: [...common, SchedulerRegistry],
96+
})
97+
export class MicroservicesModule extends BaseModule {
98+
getWorker() {
99+
return ImmichWorker.MICROSERVICES;
112100
}
113101
}
114102

server/src/interfaces/event.interface.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1+
import { ClassConstructor } from 'class-transformer';
12
import { SystemConfig } from 'src/config';
23
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
34
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
5+
import { ImmichWorker } from 'src/enum';
46

57
export const IEventRepository = 'IEventRepository';
68

79
type EventMap = {
810
// app events
9-
'app.bootstrap': ['api' | 'microservices'];
10-
'app.shutdown': [];
11+
'app.bootstrap': [ImmichWorker];
12+
'app.shutdown': [ImmichWorker];
1113

1214
// config events
1315
'config.update': [
@@ -85,6 +87,7 @@ export type EventItem<T extends EmitEvent> = {
8587
};
8688

8789
export interface IEventRepository {
90+
setup(options: { services: ClassConstructor<unknown>[] }): void;
8891
on<T extends keyof EventMap>(item: EventItem<T>): void;
8992
emit<T extends keyof EventMap>(event: T, ...args: ArgsOf<T>): Promise<void>;
9093

server/src/interfaces/logger.interface.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { LogLevel } from 'src/enum';
1+
import { ImmichWorker, LogLevel } from 'src/enum';
22

33
export const ILoggerRepository = 'ILoggerRepository';
44

55
export interface ILoggerRepository {
6-
setAppName(name: string): void;
6+
setAppName(name: ImmichWorker): void;
77
setContext(message: string): void;
88
setLogLevel(level: LogLevel | false): void;
99
isLevelEnabled(level: LogLevel): boolean;

server/src/repositories/event.repository.ts

+57-1
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
import { Inject, Injectable } from '@nestjs/common';
2-
import { ModuleRef } from '@nestjs/core';
2+
import { ModuleRef, Reflector } from '@nestjs/core';
33
import {
44
OnGatewayConnection,
55
OnGatewayDisconnect,
66
OnGatewayInit,
77
WebSocketGateway,
88
WebSocketServer,
99
} from '@nestjs/websockets';
10+
import { ClassConstructor } from 'class-transformer';
11+
import _ from 'lodash';
1012
import { Server, Socket } from 'socket.io';
13+
import { EventConfig } from 'src/decorators';
14+
import { MetadataKey } from 'src/enum';
1115
import {
1216
ArgsOf,
1317
ClientEventMap,
1418
EmitEvent,
19+
EmitHandler,
1520
EventItem,
1621
IEventRepository,
1722
serverEvents,
@@ -24,6 +29,14 @@ import { handlePromiseError } from 'src/utils/misc';
2429

2530
type EmitHandlers = Partial<{ [T in EmitEvent]: Array<EventItem<T>> }>;
2631

32+
type Item<T extends EmitEvent> = {
33+
event: T;
34+
handler: EmitHandler<T>;
35+
priority: number;
36+
server: boolean;
37+
label: string;
38+
};
39+
2740
@Instrumentation()
2841
@WebSocketGateway({
2942
cors: true,
@@ -44,6 +57,49 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect
4457
this.logger.setContext(EventRepository.name);
4558
}
4659

60+
setup({ services }: { services: ClassConstructor<unknown>[] }) {
61+
const reflector = this.moduleRef.get(Reflector, { strict: false });
62+
const repository = this.moduleRef.get<IEventRepository>(IEventRepository);
63+
const items: Item<EmitEvent>[] = [];
64+
65+
// discovery
66+
for (const Service of services) {
67+
const instance = this.moduleRef.get<any>(Service);
68+
const ctx = Object.getPrototypeOf(instance);
69+
for (const property of Object.getOwnPropertyNames(ctx)) {
70+
const descriptor = Object.getOwnPropertyDescriptor(ctx, property);
71+
if (!descriptor || descriptor.get || descriptor.set) {
72+
continue;
73+
}
74+
75+
const handler = instance[property];
76+
if (typeof handler !== 'function') {
77+
continue;
78+
}
79+
80+
const event = reflector.get<EventConfig>(MetadataKey.EVENT_CONFIG, handler);
81+
if (!event) {
82+
continue;
83+
}
84+
85+
items.push({
86+
event: event.name,
87+
priority: event.priority || 0,
88+
server: event.server ?? false,
89+
handler: handler.bind(instance),
90+
label: `${Service.name}.${handler.name}`,
91+
});
92+
}
93+
}
94+
95+
const handlers = _.orderBy(items, ['priority'], ['asc']);
96+
97+
// register by priority
98+
for (const handler of handlers) {
99+
repository.on(handler);
100+
}
101+
}
102+
47103
afterInit(server: Server) {
48104
this.logger.log('Initialized websocket server');
49105

server/src/repositories/logger.repository.spec.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ClsService } from 'nestjs-cls';
2+
import { ImmichWorker } from 'src/enum';
23
import { IConfigRepository } from 'src/interfaces/config.interface';
34
import { LoggerRepository } from 'src/repositories/logger.repository';
45
import { mockEnvData, newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
@@ -22,18 +23,18 @@ describe(LoggerRepository.name, () => {
2223
configMock.getEnv.mockReturnValue(mockEnvData({ noColor: false }));
2324

2425
sut = new LoggerRepository(clsMock, configMock);
25-
sut.setAppName('api');
26+
sut.setAppName(ImmichWorker.API);
2627

27-
expect(sut['formatContext']('context')).toBe('\u001B[33m[api:context]\u001B[39m ');
28+
expect(sut['formatContext']('context')).toBe('\u001B[33m[Api:context]\u001B[39m ');
2829
});
2930

3031
it('should not use colors when noColor is true', () => {
3132
configMock.getEnv.mockReturnValue(mockEnvData({ noColor: true }));
3233

3334
sut = new LoggerRepository(clsMock, configMock);
34-
sut.setAppName('api');
35+
sut.setAppName(ImmichWorker.API);
3536

36-
expect(sut['formatContext']('context')).toBe('[api:context] ');
37+
expect(sut['formatContext']('context')).toBe('[Api:context] ');
3738
});
3839
});
3940
});

server/src/repositories/logger.repository.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export class LoggerRepository extends ConsoleLogger implements ILoggerRepository
3434
private static appName?: string = undefined;
3535

3636
setAppName(name: string): void {
37-
LoggerRepository.appName = name;
37+
LoggerRepository.appName = name.charAt(0).toUpperCase() + name.slice(1);
3838
}
3939

4040
isLevelEnabled(level: LogLevel) {

server/src/services/job.service.spec.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { BadRequestException } from '@nestjs/common';
22
import { defaults } from 'src/config';
3+
import { ImmichWorker } from 'src/enum';
34
import { IAssetRepository } from 'src/interfaces/asset.interface';
45
import {
56
IJobRepository,
@@ -40,7 +41,7 @@ describe(JobService.name, () => {
4041

4142
describe('onConfigUpdate', () => {
4243
it('should update concurrency', () => {
43-
sut.onBootstrap('microservices');
44+
sut.onBootstrap(ImmichWorker.MICROSERVICES);
4445
sut.onConfigUpdate({ oldConfig: defaults, newConfig: defaults });
4546

4647
expect(jobMock.setConcurrency).toHaveBeenCalledTimes(14);

server/src/services/job.service.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { snakeCase } from 'lodash';
33
import { OnEvent } from 'src/decorators';
44
import { mapAsset } from 'src/dtos/asset-response.dto';
55
import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto';
6-
import { AssetType, ManualJobName } from 'src/enum';
6+
import { AssetType, ImmichWorker, ManualJobName } from 'src/enum';
77
import { ArgOf } from 'src/interfaces/event.interface';
88
import {
99
ConcurrentQueueName,
@@ -43,7 +43,7 @@ export class JobService extends BaseService {
4343

4444
@OnEvent({ name: 'app.bootstrap' })
4545
onBootstrap(app: ArgOf<'app.bootstrap'>) {
46-
this.isMicroservices = app === 'microservices';
46+
this.isMicroservices = app === ImmichWorker.MICROSERVICES;
4747
}
4848

4949
@OnEvent({ name: 'config.update', server: true })

server/src/services/metadata.service.spec.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { randomBytes } from 'node:crypto';
33
import { Stats } from 'node:fs';
44
import { constants } from 'node:fs/promises';
55
import { ExifEntity } from 'src/entities/exif.entity';
6-
import { AssetType, SourceType } from 'src/enum';
6+
import { AssetType, ImmichWorker, SourceType } from 'src/enum';
77
import { IAlbumRepository } from 'src/interfaces/album.interface';
88
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
99
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
@@ -73,7 +73,7 @@ describe(MetadataService.name, () => {
7373

7474
describe('onBootstrapEvent', () => {
7575
it('should pause and resume queue during init', async () => {
76-
await sut.onBootstrap('microservices');
76+
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
7777

7878
expect(jobMock.pause).toHaveBeenCalledTimes(1);
7979
expect(mapMock.init).toHaveBeenCalledTimes(1);
@@ -83,7 +83,7 @@ describe(MetadataService.name, () => {
8383
it('should return if reverse geocoding is disabled', async () => {
8484
systemMock.get.mockResolvedValue({ reverseGeocoding: { enabled: false } });
8585

86-
await sut.onBootstrap('microservices');
86+
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
8787

8888
expect(jobMock.pause).not.toHaveBeenCalled();
8989
expect(mapMock.init).not.toHaveBeenCalled();

server/src/services/metadata.service.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { AssetFaceEntity } from 'src/entities/asset-face.entity';
1212
import { AssetEntity } from 'src/entities/asset.entity';
1313
import { ExifEntity } from 'src/entities/exif.entity';
1414
import { PersonEntity } from 'src/entities/person.entity';
15-
import { AssetType, SourceType } from 'src/enum';
15+
import { AssetType, ImmichWorker, SourceType } from 'src/enum';
1616
import { WithoutProperty } from 'src/interfaces/asset.interface';
1717
import { DatabaseLock } from 'src/interfaces/database.interface';
1818
import { ArgOf } from 'src/interfaces/event.interface';
@@ -89,7 +89,7 @@ const validateRange = (value: number | undefined, min: number, max: number): Non
8989
export class MetadataService extends BaseService {
9090
@OnEvent({ name: 'app.bootstrap' })
9191
async onBootstrap(app: ArgOf<'app.bootstrap'>) {
92-
if (app !== 'microservices') {
92+
if (app !== ImmichWorker.MICROSERVICES) {
9393
return;
9494
}
9595
const config = await this.getConfig({ withCache: false });

server/src/services/microservices.service.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Injectable } from '@nestjs/common';
22
import { OnEvent } from 'src/decorators';
3+
import { ImmichWorker } from 'src/enum';
34
import { ArgOf } from 'src/interfaces/event.interface';
45
import { IDeleteFilesJob, JobName } from 'src/interfaces/job.interface';
56
import { AssetService } from 'src/services/asset.service';
@@ -45,7 +46,7 @@ export class MicroservicesService {
4546

4647
@OnEvent({ name: 'app.bootstrap' })
4748
async onBootstrap(app: ArgOf<'app.bootstrap'>) {
48-
if (app !== 'microservices') {
49+
if (app !== ImmichWorker.MICROSERVICES) {
4950
return;
5051
}
5152

0 commit comments

Comments
 (0)