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

@@ -0,0 +1,115 @@
/**
* Compression control decorators (@Compress, @NoCompress)
*
* These decorators allow per-route control over compression:
* - @Compress(options?) - Force compression with optional settings
* - @NoCompress() - Disable compression for this route (useful for SSE, streaming)
*/
import { getControllerMetadata } from './decorators.metadata.js';
import type { IRouteCompressionOptions } from './decorators.types.js';
/**
* Set compression options for a route
*/
function setRouteCompression(
target: any,
methodName: string | symbol,
options: IRouteCompressionOptions
): void {
const metadata = getControllerMetadata(target.constructor);
let route = metadata.routes.get(methodName);
if (!route) {
// Create placeholder route (will be completed by @Get/@Post/etc.)
route = {
method: 'GET',
path: '',
methodName,
interceptors: [],
options: {},
};
metadata.routes.set(methodName, route);
}
route.compression = options;
}
/**
* @Compress decorator - Force compression on a route with optional settings
*
* @example
* ```typescript
* @Controller('/api')
* class ApiController {
* @Get('/heavy-data')
* @Compress({ level: 11 }) // Max brotli compression
* getHeavyData() {
* return massiveJsonData;
* }
*
* @Get('/data')
* @Compress() // Use default settings
* getData() {
* return jsonData;
* }
* }
* ```
*/
export function Compress(options?: { level?: number }) {
return function <This, Args extends any[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
context.addInitializer(function (this: This) {
setRouteCompression(this, context.name, {
enabled: true,
level: options?.level,
});
});
return target;
};
}
/**
* @NoCompress decorator - Disable compression for a route
*
* Useful for:
* - Server-Sent Events (SSE)
* - Streaming responses
* - Already-compressed content
* - Real-time data where latency is critical
*
* @example
* ```typescript
* @Controller('/api')
* class EventController {
* @Get('/events')
* @NoCompress() // SSE should not be compressed
* getEvents() {
* return new Response(eventStream, {
* headers: { 'Content-Type': 'text/event-stream' }
* });
* }
*
* @Get('/video/:id')
* @NoCompress() // Already compressed media
* getVideo() {
* return videoStream;
* }
* }
* ```
*/
export function NoCompress() {
return function <This, Args extends any[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
context.addInitializer(function (this: This) {
setRouteCompression(this, context.name, {
enabled: false,
});
});
return target;
};
}

View File

@@ -102,6 +102,7 @@ export class ControllerRegistry {
method: route.method,
handler,
interceptors,
compression: route.compression,
});
}
}

View File

@@ -10,6 +10,7 @@ import type {
IMethodOptions,
IRouteOptions,
} from '../core/smartserve.interfaces.js';
import type { ICompressionConfig } from '../compression/index.js';
// =============================================================================
// Metadata Types
@@ -29,6 +30,16 @@ export interface IControllerMetadata {
target?: new (...args: any[]) => any;
}
/**
* Route compression options
*/
export interface IRouteCompressionOptions {
/** Whether compression is enabled for this route (undefined = use default) */
enabled?: boolean;
/** Override compression level */
level?: number;
}
/**
* Metadata for individual route methods
*/
@@ -45,6 +56,8 @@ export interface IRouteMetadata {
methodName: string | symbol;
/** Handler function reference */
handler?: Function;
/** Route-specific compression settings */
compression?: IRouteCompressionOptions;
}
/**
@@ -73,4 +86,6 @@ export interface ICompiledRoute {
handler: (ctx: IRequestContext) => Promise<any>;
/** Combined interceptors (class + method) */
interceptors: IInterceptOptions[];
/** Route-specific compression settings */
compression?: IRouteCompressionOptions;
}

View File

@@ -2,6 +2,7 @@
export type {
IControllerMetadata,
IRouteMetadata,
IRouteCompressionOptions,
IRegisteredController,
ICompiledRoute,
} from './decorators.types.js';
@@ -35,6 +36,9 @@ export {
addTimestamp,
} from './decorators.interceptors.js';
// Compression decorators
export { Compress, NoCompress } from './decorators.compress.js';
// Registry
export { ControllerRegistry } from './decorators.registry.js';