feat(compression): Add cross-runtime response compression (Brotli/gzip), per-route decorators, and pre-compressed static file support
This commit is contained in:
115
ts/decorators/decorators.compress.ts
Normal file
115
ts/decorators/decorators.compress.ts
Normal 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;
|
||||
};
|
||||
}
|
||||
@@ -102,6 +102,7 @@ export class ControllerRegistry {
|
||||
method: route.method,
|
||||
handler,
|
||||
interceptors,
|
||||
compression: route.compression,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user