Files
fluxer/packages/api/src/app/APILifecycle.tsx
2026-02-17 12:22:36 +00:00

150 lines
5.5 KiB
TypeScript

/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {randomUUID} from 'node:crypto';
import type {APIConfig} from '@fluxer/api/src/config/APIConfig';
import {GuildDataRepository} from '@fluxer/api/src/guild/repositories/GuildDataRepository';
import type {ILogger} from '@fluxer/api/src/ILogger';
import {KVAccountDeletionQueueService} from '@fluxer/api/src/infrastructure/KVAccountDeletionQueueService';
import {initializeMetricsService} from '@fluxer/api/src/infrastructure/MetricsService';
import {InstanceConfigRepository} from '@fluxer/api/src/instance/InstanceConfigRepository';
import {ipBanCache} from '@fluxer/api/src/middleware/IpBanMiddleware';
import {initializeServiceSingletons} from '@fluxer/api/src/middleware/ServiceMiddleware';
import {ensureVoiceResourcesInitialized, getKVClient} from '@fluxer/api/src/middleware/ServiceRegistry';
import {ReportRepository} from '@fluxer/api/src/report/ReportRepository';
import {initializeSearch, shutdownSearch} from '@fluxer/api/src/SearchFactory';
import {warmupAdminSearchIndexes} from '@fluxer/api/src/search/SearchWarmup';
import {VisionarySlotInitializer} from '@fluxer/api/src/stripe/VisionarySlotInitializer';
import {UserRepository} from '@fluxer/api/src/user/repositories/UserRepository';
import {VoiceDataInitializer} from '@fluxer/api/src/voice/VoiceDataInitializer';
export function createInitializer(config: APIConfig, logger: ILogger): () => Promise<void> {
return async (): Promise<void> => {
logger.info('Initializing API service...');
const kvClient = getKVClient();
ipBanCache.setRefreshSubscriber(kvClient);
await ipBanCache.initialize();
logger.info('IP ban cache initialized');
initializeMetricsService();
logger.info('Metrics service initialized');
await initializeServiceSingletons();
logger.info('Service singletons initialized');
try {
const userRepository = new UserRepository();
const kvDeletionQueue = new KVAccountDeletionQueueService(kvClient, userRepository);
if (await kvDeletionQueue.needsRebuild()) {
logger.warn('KV deletion queue needs rebuild, rebuilding...');
await kvDeletionQueue.rebuildState();
} else {
logger.info('KV deletion queue state is healthy');
}
} catch (error) {
logger.error({error}, 'Failed to verify KV deletion queue state');
throw error;
}
logger.info({search_url: config.search.url}, 'Initializing search...');
await initializeSearch();
logger.info('Search initialized');
// All API replicas share the same Meilisearch cluster, so only one should warm it.
const warmupLockKey = 'fluxer:search:warmup:admin';
const warmupLockToken = randomUUID();
const warmupLockTtlSeconds = 60 * 60;
const acquiredWarmupLock = await kvClient.setnx(warmupLockKey, warmupLockToken, warmupLockTtlSeconds);
if (!acquiredWarmupLock) {
logger.info('Another API instance is warming search indexes, skipping warmup');
} else {
try {
await warmupAdminSearchIndexes({
userRepository: new UserRepository(),
guildRepository: new GuildDataRepository(),
reportRepository: new ReportRepository(),
logger,
});
} catch (error) {
logger.error({error}, 'Admin search warmup failed (continuing startup)');
} finally {
try {
await kvClient.releaseLock(warmupLockKey, warmupLockToken);
} catch (error) {
logger.warn({error}, 'Failed to release admin search warmup lock');
}
}
}
if (config.voice.enabled && config.voice.defaultRegion) {
const voiceDataInitializer = new VoiceDataInitializer();
await voiceDataInitializer.initialize();
await ensureVoiceResourcesInitialized();
logger.info('Voice data initialized');
}
if (config.dev.testModeEnabled && config.stripe.enabled) {
const visionarySlotInitializer = new VisionarySlotInitializer();
await visionarySlotInitializer.initialize();
logger.info('Stripe visionary slots initialized');
}
if (config.dev.testModeEnabled) {
const instanceConfigRepository = new InstanceConfigRepository();
try {
await instanceConfigRepository.setSsoConfig({
enabled: false,
authorizationUrl: null,
tokenUrl: null,
clientId: null,
});
logger.info('Reset SSO config to disabled for test mode');
} catch (error) {
logger.warn({error}, 'Failed to reset SSO config for test mode');
}
}
logger.info('API service initialization complete');
};
}
export function createShutdown(logger: ILogger): () => Promise<void> {
return async (): Promise<void> => {
logger.info('Shutting down API service...');
try {
await shutdownSearch();
logger.info('Search service shut down');
} catch (error) {
logger.error({error}, 'Error shutting down search service');
}
try {
ipBanCache.shutdown();
logger.info('IP ban cache shut down');
} catch (error) {
logger.error({error}, 'Error shutting down IP ban cache');
}
logger.info('API service shutdown complete');
};
}