fluxer/fluxer_server/src/Routes.tsx
2026-02-17 12:22:36 +00:00

199 lines
5.8 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 type {Config} from '@app/Config';
import {createHealthCheckHandler, createLivenessCheckHandler, createReadinessCheckHandler} from '@app/HealthCheck';
import {createComponentLogger} from '@app/Logger';
import {
type InitializedServices,
initializeAllServices,
runServiceInitialization,
type ServiceInitializer,
shutdownAllServices,
startBackgroundServices,
} from '@app/ServiceInitializer';
import {getBuildMetadata} from '@fluxer/config/src/BuildMetadata';
import {AppErrorHandler, AppNotFoundHandler} from '@fluxer/errors/src/domains/core/ErrorHandlers';
import {applyMiddlewareStack} from '@fluxer/hono/src/middleware/MiddlewareStack';
import {createServiceTelemetry} from '@fluxer/hono/src/middleware/TelemetryAdapters';
import type {BaseHonoEnv} from '@fluxer/hono_types/src/HonoTypes';
import {Hono} from 'hono';
import {trimTrailingSlash} from 'hono/trailing-slash';
export interface MountedRoutes {
app: Hono<BaseHonoEnv>;
services: InitializedServices;
initialize: () => Promise<void>;
start: () => Promise<void>;
shutdown: () => Promise<void>;
}
export interface MountRoutesOptions {
config: Config;
staticDir?: string | undefined;
}
const startTime = Date.now();
const BUILD_METADATA = getBuildMetadata();
export async function mountRoutes(options: MountRoutesOptions): Promise<MountedRoutes> {
const {config, staticDir} = options;
const logger = createComponentLogger('routes');
const VERSION = BUILD_METADATA.buildNumber ?? '0.0.0';
logger.info('Starting route mounting and service initialization');
const app = new Hono<BaseHonoEnv>();
app.use(trimTrailingSlash());
const telemetry = createServiceTelemetry({
serviceName: 'fluxer-server',
skipPaths: ['/_health', '/_ready', '/_live'],
});
applyMiddlewareStack(app, {
requestId: {},
tracing: telemetry.tracing,
metrics: {
enabled: true,
collector: telemetry.metricsCollector,
skipPaths: ['/_health', '/_ready', '/_live'],
},
logger: {
log: (data) => {
logger.info(
{
method: data.method,
path: data.path,
status: data.status,
durationMs: data.durationMs,
},
'Request completed',
);
},
skip: ['/_health', '/_ready', '/_live'],
},
skipErrorHandler: true,
});
let initializers: Array<ServiceInitializer> = [];
let services: InitializedServices = {};
try {
const result = await initializeAllServices({
config,
logger,
staticDir,
});
initializers = result.initializers;
services = result.services;
if (services.s3 !== undefined) {
app.route('/s3', services.s3.app);
logger.info(config.isMonolith ? 'S3 service mounted at /s3 (restricted mode)' : 'S3 service mounted at /s3');
}
if (services.queue !== undefined && !config.isMonolith) {
app.route('/queue', services.queue.app);
logger.info('Queue service mounted at /queue');
} else if (services.queue !== undefined) {
logger.info('Queue service available internally only (monolith mode)');
}
if (services.mediaProxy !== undefined) {
app.route('/media', services.mediaProxy.app);
logger.info(
config.isMonolith
? 'Media Proxy service mounted at /media (public-only mode)'
: 'Media Proxy service mounted at /media',
);
}
if (services.admin !== undefined) {
app.route('/admin', services.admin.app);
logger.info('Admin service mounted at /admin');
}
if (services.api !== undefined) {
const apiService = services.api;
app.route('/api', apiService.app);
app.get('/.well-known/fluxer', (ctx) => apiService.app.fetch(ctx.req.raw));
logger.info('API service mounted at /api');
}
const healthHandler = createHealthCheckHandler({
services,
staticDir,
version: VERSION,
startTime,
latencyThresholdMs: config.healthCheck.latencyThresholdMs,
});
app.get('/_health', healthHandler);
logger.info('Health check endpoint mounted at /_health');
const readinessHandler = createReadinessCheckHandler({
services,
staticDir,
version: VERSION,
startTime,
latencyThresholdMs: config.healthCheck.latencyThresholdMs,
});
app.get('/_ready', readinessHandler);
logger.info('Readiness check endpoint mounted at /_ready');
const livenessHandler = createLivenessCheckHandler();
app.get('/_live', livenessHandler);
logger.info('Liveness check endpoint mounted at /_live');
if (services.appServer !== undefined) {
app.route('/', services.appServer.app);
logger.info('SPA App server mounted at /');
}
app.onError(AppErrorHandler);
app.notFound(AppNotFoundHandler);
logger.info({serviceCount: initializers.length}, 'All services mounted successfully');
} catch (error) {
logger.error({error: error instanceof Error ? error.message : 'Unknown error'}, 'Failed to mount routes');
throw error;
}
const initialize = async (): Promise<void> => {
await runServiceInitialization(initializers, logger);
};
const start = async (): Promise<void> => {
await startBackgroundServices(initializers, logger);
};
const shutdown = async (): Promise<void> => {
await shutdownAllServices(initializers, logger);
};
return {
app,
services,
initialize,
start,
shutdown,
};
}