feat(compression): Add cross-runtime response compression (Brotli/gzip), per-route decorators, and pre-compressed static file support

This commit is contained in:
2025-12-05 12:27:41 +00:00
parent cef6ce750e
commit 57d7fd6483
17 changed files with 1116 additions and 18 deletions

View File

@@ -18,9 +18,16 @@ import type {
} from './smartserve.interfaces.js';
import { HttpError, RouteNotFoundError, ServerAlreadyRunningError, WebSocketConfigError } from './smartserve.errors.js';
import { AdapterFactory, type BaseAdapter, type TRequestHandler } from '../adapters/index.js';
import { ControllerRegistry, type ICompiledRoute } from '../decorators/index.js';
import { ControllerRegistry, type ICompiledRoute, type IRouteCompressionOptions } from '../decorators/index.js';
import { FileServer } from '../files/index.js';
import { WebDAVHandler } from '../protocols/index.js';
import {
normalizeCompressionConfig,
shouldCompressResponse,
selectCompressionAlgorithm,
compressResponse,
type ICompressionConfig,
} from '../compression/index.js';
/**
* SmartServe - Cross-platform HTTP server
@@ -43,6 +50,7 @@ export class SmartServe {
private customHandler: TRequestHandler | null = null;
private fileServer: FileServer | null = null;
private webdavHandler: WebDAVHandler | null = null;
private compressionConfig: ICompressionConfig;
/** WebSocket connection registry (only active when typedRouter is set) */
private wsConnections: Map<string, IWebSocketPeer> | null = null;
@@ -64,6 +72,9 @@ export class SmartServe {
...options,
};
// Initialize compression config (enabled by default)
this.compressionConfig = normalizeCompressionConfig(options.compression);
// Initialize connection registry only when typedRouter is configured
if (this.options.websocket?.typedRouter) {
this.wsConnections = new Map();
@@ -248,7 +259,9 @@ export class SmartServe {
return async (request: Request, connectionInfo: IConnectionInfo): Promise<Response> => {
// Use custom handler if set
if (this.customHandler) {
return this.customHandler(request, connectionInfo);
const response = await this.customHandler(request, connectionInfo);
// Apply compression to custom handler responses
return this.applyCompression(response, request);
}
// Parse URL and method
@@ -258,7 +271,8 @@ export class SmartServe {
// Handle WebDAV requests first if handler is configured
if (this.webdavHandler && this.webdavHandler.isWebDAVRequest(request)) {
try {
return await this.webdavHandler.handle(request);
const response = await this.webdavHandler.handle(request);
return this.applyCompression(response, request);
} catch (error) {
return this.handleError(error as Error, request);
}
@@ -271,7 +285,8 @@ export class SmartServe {
// No route found, try WebDAV for GET/PUT/DELETE/HEAD (standard HTTP methods WebDAV also handles)
if (this.webdavHandler) {
try {
return await this.webdavHandler.handle(request);
const response = await this.webdavHandler.handle(request);
return this.applyCompression(response, request);
} catch (error) {
return this.handleError(error as Error, request);
}
@@ -282,7 +297,8 @@ export class SmartServe {
try {
const staticResponse = await this.fileServer.serve(request);
if (staticResponse) {
return staticResponse;
// Apply compression to static file responses
return this.applyCompression(staticResponse, request);
}
} catch (error) {
return this.handleError(error as Error, request);
@@ -304,7 +320,10 @@ export class SmartServe {
const context = await this.createContext(request, url, params, connectionInfo);
// Run interceptors and handler
return await this.executeRoute(route, context);
const response = await this.executeRoute(route, context);
// Apply compression with route-specific settings
return this.applyCompression(response, request, route.compression);
} catch (error) {
return this.handleError(error as Error, request);
}
@@ -456,6 +475,50 @@ export class SmartServe {
});
}
/**
* Apply compression to response if applicable
*/
private async applyCompression(
response: Response,
request: Request,
routeCompression?: IRouteCompressionOptions
): Promise<Response> {
// Check route-level override first
if (routeCompression?.enabled === false) {
return response;
}
// Build effective config (merge route settings with global)
const effectiveConfig: ICompressionConfig = {
...this.compressionConfig,
};
// Route-level compression settings override global
if (routeCompression?.level !== undefined) {
effectiveConfig.level = routeCompression.level;
}
// If route forces compression, ensure it's enabled
if (routeCompression?.enabled === true) {
effectiveConfig.enabled = true;
}
// Check if compression should be applied
if (!shouldCompressResponse(response, request, effectiveConfig)) {
return response;
}
// Select best algorithm
const algorithm = selectCompressionAlgorithm(request, effectiveConfig);
if (algorithm === 'identity') {
return response;
}
// Apply compression
return compressResponse(response, algorithm, effectiveConfig.level);
}
/**
* Handle errors
*/

View File

@@ -4,6 +4,7 @@
*/
import type { TypedRouter } from '@api.global/typedrequest';
import type { ICompressionConfig } from '../compression/index.js';
// =============================================================================
// HTTP Types
@@ -272,6 +273,8 @@ export interface IStaticOptions {
extensions?: string[];
/** Enable directory listing */
directoryListing?: boolean | IDirectoryListingOptions;
/** Serve pre-compressed files (.br, .gz) when available */
precompressed?: boolean;
}
/**
@@ -333,6 +336,8 @@ export interface ISmartServeOptions {
keepAlive?: IKeepAliveConfig;
/** Global error handler */
onError?: (error: Error, request?: Request) => Response | Promise<Response>;
/** Compression configuration (enabled by default) */
compression?: ICompressionConfig | boolean;
}
// =============================================================================