Refactor speed test integration and enhance controller functionality

* Replaced SpeedTestApiRoutes with SpeedTestController for improved API handling.
* Introduced SpeedTestService and InMemoryRequestRepository for better service layer management.
* Added RandomData class for generating random data with hash support.
* Updated error handling and response structure in SpeedTestController.
* Enhanced type safety in interfaces for speed test operations.
This commit is contained in:
2025-11-21 06:29:12 -05:00
parent 2ac8e505f6
commit cd1d9ae591
5 changed files with 64 additions and 21 deletions

View File

@@ -1,7 +1,7 @@
import type {Server as BunServer, BunRequest} from 'bun';
import type {BunMethodRoutes} from '../net/http/BunHttpServer';
import {LoggerFactory} from '@techniker-me/logger';
import type { ISpeedTestService } from '../interfaces/ISpeedTest';
import type { ISpeedTestService } from '../core/interfaces/ISpeedTestService';
export class SpeedTestController {
private readonly _logger = LoggerFactory.getLogger('SpeedTestController');
@@ -39,7 +39,7 @@ export class SpeedTestController {
try {
const receivedData = new Uint8Array(await request.arrayBuffer());
const result = await this._service.validateUpload(requestId, receivedData);
const result = await this._service.validateTest(requestId, receivedData);
return new Response(JSON.stringify(result), {
status: result.valid ? 200 : 400,
@@ -50,10 +50,9 @@ export class SpeedTestController {
this._logger.error('Error processing speedtest data: [%s]', error);
// Distinguish between user error (404, 400) and server error (500)
// Simplification: if message is known (from service), return 400 or 404.
let status = 500;
if (error.message === 'Request not found or expired') status = 404;
if (error.message.startsWith('Data size mismatch')) status = 400;
if (error.message?.startsWith('Data size mismatch')) status = 400;
return new Response(JSON.stringify({error: error.message || 'Failed to process request'}), {status, headers: {'Content-Type': 'application/json'}});
}
@@ -62,25 +61,29 @@ export class SpeedTestController {
private async getData(request: BunRequest, _server: BunServer<unknown>): Promise<Response> {
const url = new URL(request.url);
const size = parseInt(url.searchParams.get('size') ?? this.defaultSize.toString());
const timeout = url.searchParams.get('timeout') ? parseInt(url.searchParams.get('timeout')!) : undefined;
const timeout = url.searchParams.get('timeout');
const options: Record<string, string> = {};
if (timeout) options['timeout'] = timeout;
if (url.searchParams.get('hashAlgorithm')) options['hashAlgorithm'] = url.searchParams.get('hashAlgorithm')!;
if (url.searchParams.get('hashEncoding')) options['hashEncoding'] = url.searchParams.get('hashEncoding')!;
if (url.searchParams.get('chunkSize')) options['chunkSize'] = url.searchParams.get('chunkSize')!;
try {
const { session, data } = await this._service.generateTestData(size, { timeout });
const result = await this._service.initiateTest(size, options);
// Convert DataView to Uint8Array for Response
const dataArray = new Uint8Array(result.data.buffer, result.data.byteOffset, result.data.byteLength);
const headers = new Headers();
headers.set('Content-Type', 'application/octet-stream');
headers.set('Content-Length', session.size.toString());
headers.set('Content-Hash', session.hash);
headers.set('Content-Hash-Algorithm', session.hashAlgorithm);
headers.set('Content-Hash-Encoding', session.hashEncoding);
headers.set('Content-Chunk-Size', session.chunkSize.toString());
headers.set('X-Request-ID', session.requestId);
Object.entries(result.headers).forEach(([key, value]) => {
headers.set(key, value);
});
return new Response(data, {status: 200, headers});
return new Response(dataArray, {status: 200, headers});
} catch (error: any) {
this._logger.error('Error generating data: [%s]', error);
return new Response(JSON.stringify({error: 'Failed to generate data'}), {status: 500, headers: {'Content-Type': 'application/json'}});
}
}
}

34
src/data/RandomData.ts Normal file
View File

@@ -0,0 +1,34 @@
import {LoggerFactory} from '@techniker-me/logger';
import {getRandomValues, createHash} from 'node:crypto';
export class RandomData {
private readonly _logger = LoggerFactory.getLogger('RandomData');
private readonly _size: number;
private readonly _data: Uint8Array;
private readonly _dataHashAlgorithm: string;
private _dataHash: string;
constructor(size: number) {
this._size = size;
this._data = getRandomValues(new Uint8Array(size));
this._dataHashAlgorithm = 'sha256';
this._dataHash = this.generateDataHash(this._data);
}
get hash(): string {
return this._dataHash;
}
get data(): DataView {
return new DataView(this._data.buffer);
}
get dataByteLength(): number {
return this._data.byteLength;
}
private generateDataHash(data: Uint8Array): string {
return createHash(this._dataHashAlgorithm).update(data).digest('base64');
}
}

View File

@@ -1,11 +1,17 @@
import HttpServerFactory from './net/http/HttpServerFactory';
import {HealthCheckApiRoutes} from './health/HealthCheckRoute';
import HealthCheck from './health/HealthCheck';
import {SpeedTestApiRoutes} from './api/SpeedTestApiRoutes';
import {SpeedTestController} from './controllers/SpeedTestController';
import {SpeedTestService} from './services/SpeedTestService';
import {InMemoryRequestRepository} from './repositories/InMemoryRequestRepository';
const healthCheck = new HealthCheck();
const healthCheckRoutes = new HealthCheckApiRoutes(healthCheck);
const speedTestApiRoutes = new SpeedTestApiRoutes();
// Dependency Injection Composition Root
const speedTestRepository = new InMemoryRequestRepository();
const speedTestService = new SpeedTestService(speedTestRepository);
const speedTestController = new SpeedTestController(speedTestService);
// Frontend serving function - using Bun's super simple static file serving
const serveFrontend = async (_request: any, _server: any) => new Response(Bun.file('./frontend.html'), {
@@ -18,7 +24,7 @@ const serveFrontend = async (_request: any, _server: any) => new Response(Bun.fi
const server = HttpServerFactory.createBunHttpServer({
'/': serveFrontend,
...healthCheckRoutes.getGETRoute(),
...speedTestApiRoutes.getRoutes(),
...speedTestController.getRoutes(),
});
const listenPromise = server.listen(8080);

View File

@@ -1,4 +1,4 @@
import { SpeedTestSession } from '../models/SpeedTestSession';
import type { SpeedTestSession } from '../models/SpeedTestSession';
export interface ISpeedTestRepository {
save(session: SpeedTestSession): void;

View File

@@ -1,4 +1,4 @@
import { SpeedTestResult } from '../models/SpeedTestResult';
import type { SpeedTestResult } from '../models/SpeedTestResult';
export interface SpeedTestServiceInitParams {
size: number;