Files
hitstar/src/server-deno/presentation/HttpServer.ts

123 lines
3.4 KiB
TypeScript

import { Application, Router, send } from '@oak/oak';
import type { AppConfig } from '../shared/config.ts';
import { logger } from '../shared/logger.ts';
import { createTrackRoutes } from './routes/trackRoutes.ts';
import { createAudioRoutes } from './routes/audioRoutes.ts';
import type { TrackService } from '../application/mod.ts';
import type { AudioStreamingService, CoverArtService } from '../infrastructure/mod.ts';
/**
* HTTP server using Oak
*/
export class HttpServer {
private readonly app: Application;
constructor(
private readonly config: AppConfig,
private readonly trackService: TrackService,
private readonly audioStreaming: AudioStreamingService,
private readonly coverArt: CoverArtService,
) {
this.app = new Application();
this.setupMiddleware();
this.setupRoutes();
}
/**
* Setup middleware
*/
private setupMiddleware(): void {
// Error handling
this.app.use(async (ctx, next) => {
try {
await next();
} catch (error) {
logger.error(`Request error: ${error}`);
ctx.response.status = 500;
ctx.response.body = { error: 'Internal server error' };
}
});
// Logging
this.app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
logger.info(`${ctx.request.method} ${ctx.request.url.pathname} - ${ctx.response.status} - ${ms}ms`);
});
// CORS
this.app.use(async (ctx, next) => {
ctx.response.headers.set('Access-Control-Allow-Origin', this.config.corsOrigin);
ctx.response.headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
ctx.response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Range');
ctx.response.headers.set('Access-Control-Expose-Headers', 'Content-Range, Content-Length, Accept-Ranges');
if (ctx.request.method === 'OPTIONS') {
ctx.response.status = 204;
return;
}
await next();
});
}
/**
* Setup routes
*/
private setupRoutes(): void {
const router = new Router();
// API routes
const trackRoutes = createTrackRoutes(this.trackService);
const audioRoutes = createAudioRoutes(this.audioStreaming, this.coverArt, this.config);
router.use(trackRoutes.routes(), trackRoutes.allowedMethods());
router.use(audioRoutes.routes(), audioRoutes.allowedMethods());
this.app.use(router.routes());
this.app.use(router.allowedMethods());
// Static file serving (public directory)
this.app.use(async (ctx) => {
try {
await send(ctx, ctx.request.url.pathname, {
root: this.config.publicDir,
index: 'index.html',
});
} catch {
// If file not found, serve index.html for SPA routing
try {
await send(ctx, '/index.html', {
root: this.config.publicDir,
});
} catch (error) {
ctx.response.status = 404;
ctx.response.body = 'Not found';
}
}
});
}
/**
* Start the HTTP server
*/
async listen(): Promise<void> {
const { host, port } = this.config;
logger.info(`Starting HTTP server on http://${host}:${port}`);
await this.app.listen({
hostname: host,
port,
});
}
/**
* Get the underlying Oak application
*/
getApp(): Application {
return this.app;
}
}