feat(compression): Add cross-runtime response compression (Brotli/gzip), per-route decorators, and pre-compressed static file support
This commit is contained in:
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user