199 lines
5.8 KiB
TypeScript
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,
|
|
};
|
|
}
|