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:
@@ -1,7 +1,7 @@
|
|||||||
import type {Server as BunServer, BunRequest} from 'bun';
|
import type {Server as BunServer, BunRequest} from 'bun';
|
||||||
import type {BunMethodRoutes} from '../net/http/BunHttpServer';
|
import type {BunMethodRoutes} from '../net/http/BunHttpServer';
|
||||||
import {LoggerFactory} from '@techniker-me/logger';
|
import {LoggerFactory} from '@techniker-me/logger';
|
||||||
import type { ISpeedTestService } from '../interfaces/ISpeedTest';
|
import type { ISpeedTestService } from '../core/interfaces/ISpeedTestService';
|
||||||
|
|
||||||
export class SpeedTestController {
|
export class SpeedTestController {
|
||||||
private readonly _logger = LoggerFactory.getLogger('SpeedTestController');
|
private readonly _logger = LoggerFactory.getLogger('SpeedTestController');
|
||||||
@@ -39,7 +39,7 @@ export class SpeedTestController {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const receivedData = new Uint8Array(await request.arrayBuffer());
|
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), {
|
return new Response(JSON.stringify(result), {
|
||||||
status: result.valid ? 200 : 400,
|
status: result.valid ? 200 : 400,
|
||||||
@@ -50,10 +50,9 @@ export class SpeedTestController {
|
|||||||
this._logger.error('Error processing speedtest data: [%s]', error);
|
this._logger.error('Error processing speedtest data: [%s]', error);
|
||||||
|
|
||||||
// Distinguish between user error (404, 400) and server error (500)
|
// Distinguish between user error (404, 400) and server error (500)
|
||||||
// Simplification: if message is known (from service), return 400 or 404.
|
|
||||||
let status = 500;
|
let status = 500;
|
||||||
if (error.message === 'Request not found or expired') status = 404;
|
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'}});
|
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> {
|
private async getData(request: BunRequest, _server: BunServer<unknown>): Promise<Response> {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const size = parseInt(url.searchParams.get('size') ?? this.defaultSize.toString());
|
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 {
|
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();
|
const headers = new Headers();
|
||||||
headers.set('Content-Type', 'application/octet-stream');
|
Object.entries(result.headers).forEach(([key, value]) => {
|
||||||
headers.set('Content-Length', session.size.toString());
|
headers.set(key, value);
|
||||||
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);
|
|
||||||
|
|
||||||
return new Response(data, {status: 200, headers});
|
return new Response(dataArray, {status: 200, headers});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this._logger.error('Error generating data: [%s]', error);
|
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'}});
|
return new Response(JSON.stringify({error: 'Failed to generate data'}), {status: 500, headers: {'Content-Type': 'application/json'}});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
34
src/data/RandomData.ts
Normal file
34
src/data/RandomData.ts
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
12
src/index.ts
12
src/index.ts
@@ -1,11 +1,17 @@
|
|||||||
import HttpServerFactory from './net/http/HttpServerFactory';
|
import HttpServerFactory from './net/http/HttpServerFactory';
|
||||||
import {HealthCheckApiRoutes} from './health/HealthCheckRoute';
|
import {HealthCheckApiRoutes} from './health/HealthCheckRoute';
|
||||||
import HealthCheck from './health/HealthCheck';
|
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 healthCheck = new HealthCheck();
|
||||||
const healthCheckRoutes = new HealthCheckApiRoutes(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
|
// Frontend serving function - using Bun's super simple static file serving
|
||||||
const serveFrontend = async (_request: any, _server: any) => new Response(Bun.file('./frontend.html'), {
|
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({
|
const server = HttpServerFactory.createBunHttpServer({
|
||||||
'/': serveFrontend,
|
'/': serveFrontend,
|
||||||
...healthCheckRoutes.getGETRoute(),
|
...healthCheckRoutes.getGETRoute(),
|
||||||
...speedTestApiRoutes.getRoutes(),
|
...speedTestController.getRoutes(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const listenPromise = server.listen(8080);
|
const listenPromise = server.listen(8080);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { SpeedTestSession } from '../models/SpeedTestSession';
|
import type { SpeedTestSession } from '../models/SpeedTestSession';
|
||||||
|
|
||||||
export interface ISpeedTestRepository {
|
export interface ISpeedTestRepository {
|
||||||
save(session: SpeedTestSession): void;
|
save(session: SpeedTestSession): void;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { SpeedTestResult } from '../models/SpeedTestResult';
|
import type { SpeedTestResult } from '../models/SpeedTestResult';
|
||||||
|
|
||||||
export interface SpeedTestServiceInitParams {
|
export interface SpeedTestServiceInitParams {
|
||||||
size: number;
|
size: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user