From 23bd27eb30308228b1e09165ccfcec6dfcf0a7d0 Mon Sep 17 00:00:00 2001 From: izzy Date: Fri, 19 Dec 2025 15:23:48 +0000 Subject: [PATCH] refactor: move one shot into app.repository --- server/src/repositories/app.repository.ts | 27 +++++++++++++++++++++++ server/src/services/cli.service.spec.ts | 16 ++++++++------ server/src/services/cli.service.ts | 15 +++++-------- server/src/utils/maintenance.ts | 27 ----------------------- 4 files changed, 41 insertions(+), 44 deletions(-) diff --git a/server/src/repositories/app.repository.ts b/server/src/repositories/app.repository.ts index e6181ef7f3..96e413232f 100644 --- a/server/src/repositories/app.repository.ts +++ b/server/src/repositories/app.repository.ts @@ -1,5 +1,10 @@ import { Injectable } from '@nestjs/common'; +import { createAdapter } from '@socket.io/redis-adapter'; +import Redis from 'ioredis'; +import { Server as SocketIO } from 'socket.io'; import { ExitCode } from 'src/enum'; +import { ConfigRepository } from 'src/repositories/config.repository'; +import { AppRestartEvent } from 'src/repositories/event.repository'; @Injectable() export class AppRepository { @@ -17,4 +22,26 @@ export class AppRepository { setCloseFn(fn: () => Promise) { this.closeFn = fn; } + + async sendOneShotAppRestart(state: AppRestartEvent): Promise { + const server = new SocketIO(); + const { redis } = new ConfigRepository().getEnv(); + const pubClient = new Redis({ ...redis, lazyConnect: true }); + const subClient = pubClient.duplicate(); + + await Promise.all([pubClient.connect(), subClient.connect()]); + + server.adapter(createAdapter(pubClient, subClient)); + + // => corresponds to notification.service.ts#onAppRestart + server.emit('AppRestartV1', state, async () => { + const responses = await server.serverSideEmitWithAck('AppRestart', state); + if (responses.some((response) => response !== 'ok')) { + throw new Error("One or more node(s) returned a non-'ok' response to our restart request!"); + } + + pubClient.disconnect(); + subClient.disconnect(); + }); + } } diff --git a/server/src/services/cli.service.spec.ts b/server/src/services/cli.service.spec.ts index 3987575fb0..f4f14c3e68 100644 --- a/server/src/services/cli.service.spec.ts +++ b/server/src/services/cli.service.spec.ts @@ -5,8 +5,6 @@ import { factory } from 'test/small.factory'; import { newTestService, ServiceMocks } from 'test/utils'; import { describe, it } from 'vitest'; -const mockSendRestart = vi.fn(); - describe(CliService.name, () => { let sut: CliService; let mocks: ServiceMocks; @@ -87,20 +85,22 @@ describe(CliService.name, () => { describe('disableMaintenanceMode', () => { it('should not do anything if not in maintenance mode', async () => { mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: false }); - await expect(sut.disableMaintenanceMode(mockSendRestart)).resolves.toEqual({ + await expect(sut.disableMaintenanceMode()).resolves.toEqual({ alreadyDisabled: true, }); + expect(mocks.app.sendOneShotAppRestart).toHaveBeenCalledTimes(0); expect(mocks.systemMetadata.set).toHaveBeenCalledTimes(0); expect(mocks.event.emit).toHaveBeenCalledTimes(0); }); it('should disable maintenance mode', async () => { mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: true, secret: 'secret' }); - await expect(sut.disableMaintenanceMode(mockSendRestart)).resolves.toEqual({ + await expect(sut.disableMaintenanceMode()).resolves.toEqual({ alreadyDisabled: false, }); + expect(mocks.app.sendOneShotAppRestart).toHaveBeenCalled(); expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.MaintenanceMode, { isMaintenanceMode: false, }); @@ -110,24 +110,26 @@ describe(CliService.name, () => { describe('enableMaintenanceMode', () => { it('should not do anything if in maintenance mode', async () => { mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: true, secret: 'secret' }); - await expect(sut.enableMaintenanceMode(mockSendRestart)).resolves.toEqual( + await expect(sut.enableMaintenanceMode()).resolves.toEqual( expect.objectContaining({ alreadyEnabled: true, }), ); + expect(mocks.app.sendOneShotAppRestart).toHaveBeenCalledTimes(0); expect(mocks.systemMetadata.set).toHaveBeenCalledTimes(0); expect(mocks.event.emit).toHaveBeenCalledTimes(0); }); it('should enable maintenance mode', async () => { mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: false }); - await expect(sut.enableMaintenanceMode(mockSendRestart)).resolves.toEqual( + await expect(sut.enableMaintenanceMode()).resolves.toEqual( expect.objectContaining({ alreadyEnabled: false, }), ); + expect(mocks.app.sendOneShotAppRestart).toHaveBeenCalled(); expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.MaintenanceMode, { isMaintenanceMode: true, secret: expect.stringMatching(/^\w{128}$/), @@ -139,7 +141,7 @@ describe(CliService.name, () => { it('should return a valid login URL', async () => { mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: true, secret: 'secret' }); - const result = await sut.enableMaintenanceMode(mockSendRestart); + const result = await sut.enableMaintenanceMode(); expect(result).toEqual( expect.objectContaining({ diff --git a/server/src/services/cli.service.ts b/server/src/services/cli.service.ts index 7ab81043b2..14864678cc 100644 --- a/server/src/services/cli.service.ts +++ b/server/src/services/cli.service.ts @@ -5,7 +5,7 @@ import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto'; import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto'; import { SystemMetadataKey } from 'src/enum'; import { BaseService } from 'src/services/base.service'; -import { createMaintenanceLoginUrl, generateMaintenanceSecret, sendOneShotAppRestart } from 'src/utils/maintenance'; +import { createMaintenanceLoginUrl, generateMaintenanceSecret } from 'src/utils/maintenance'; import { getExternalDomain } from 'src/utils/misc'; @Injectable() @@ -42,7 +42,7 @@ export class CliService extends BaseService { await this.updateConfig(config); } - async disableMaintenanceMode(sendAppRestartCallback = sendOneShotAppRestart): Promise<{ alreadyDisabled: boolean }> { + async disableMaintenanceMode(): Promise<{ alreadyDisabled: boolean }> { const currentState = await this.systemMetadataRepository .get(SystemMetadataKey.MaintenanceMode) .then((state) => state ?? { isMaintenanceMode: false as const }); @@ -55,17 +55,14 @@ export class CliService extends BaseService { const state = { isMaintenanceMode: false as const }; await this.systemMetadataRepository.set(SystemMetadataKey.MaintenanceMode, state); - - await sendAppRestartCallback(state); + await this.appRepository.sendOneShotAppRestart(state); return { alreadyDisabled: false, }; } - async enableMaintenanceMode( - sendAppRestartCallback = sendOneShotAppRestart, - ): Promise<{ authUrl: string; alreadyEnabled: boolean }> { + async enableMaintenanceMode(): Promise<{ authUrl: string; alreadyEnabled: boolean }> { const { server } = await this.getConfig({ withCache: true }); const baseUrl = getExternalDomain(server); @@ -91,9 +88,7 @@ export class CliService extends BaseService { secret, }); - await sendAppRestartCallback({ - isMaintenanceMode: true, - }); + await this.appRepository.sendOneShotAppRestart(state); return { authUrl: await createMaintenanceLoginUrl(baseUrl, payload, secret), diff --git a/server/src/utils/maintenance.ts b/server/src/utils/maintenance.ts index f4e6a6ce46..faa92395d6 100644 --- a/server/src/utils/maintenance.ts +++ b/server/src/utils/maintenance.ts @@ -1,33 +1,6 @@ -import { createAdapter } from '@socket.io/redis-adapter'; -import Redis from 'ioredis'; import { SignJWT } from 'jose'; import { randomBytes } from 'node:crypto'; -import { Server as SocketIO } from 'socket.io'; import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto'; -import { ConfigRepository } from 'src/repositories/config.repository'; -import { AppRestartEvent } from 'src/repositories/event.repository'; - -export async function sendOneShotAppRestart(state: AppRestartEvent): Promise { - const server = new SocketIO(); - const { redis } = new ConfigRepository().getEnv(); - const pubClient = new Redis({ ...redis, lazyConnect: true }); - const subClient = pubClient.duplicate(); - - await Promise.all([pubClient.connect(), subClient.connect()]); - - server.adapter(createAdapter(pubClient, subClient)); - - // => corresponds to notification.service.ts#onAppRestart - server.emit('AppRestartV1', state, async () => { - const responses = await server.serverSideEmitWithAck('AppRestart', state); - if (responses.some((response) => response !== 'ok')) { - throw new Error("One or more node(s) returned a non-'ok' response to our restart request!"); - } - - pubClient.disconnect(); - subClient.disconnect(); - }); -} export async function createMaintenanceLoginUrl( baseUrl: string,